Skip to content

Commit d65063c

Browse files
authored
IDP-3015 Implement CLI for IDP (#71)
1 parent ca52575 commit d65063c

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed

idp.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"regexp"
13+
"strings"
14+
15+
_ "github.com/fatih/color"
16+
"github.com/urfave/cli/v2"
17+
)
18+
19+
var currentDirectory = filepath.Base(os.Getwd())
20+
var branch = "main"
21+
22+
const yamlContentTemplate = `
23+
apiVersion: backstage.io/v1alpha1
24+
kind: Component
25+
metadata:
26+
name: {repo_name}
27+
tags:
28+
- auto-generated
29+
annotations:
30+
backstage.io/source-location: url:{repo_path}
31+
github.com/project-slug: {project_slug}
32+
spec:
33+
type: service
34+
lifecycle: experimental
35+
owner: Harness_Account_All_Users
36+
system: {orgName}
37+
`
38+
39+
func getRepositoriesAPI(organization, token, repoPattern string) ([]map[string]string, error) {
40+
url := fmt.Sprintf("https://api.github.com/orgs/%s/repos", organization)
41+
headers := map[string]string{
42+
"Accept": "application/vnd.github.v3+json",
43+
"Authorization": fmt.Sprintf("Bearer %s", token),
44+
"X-GitHub-Api-Version": "2022-11-28",
45+
}
46+
47+
var allReposInfo []map[string]string
48+
page := 1
49+
for {
50+
params := fmt.Sprintf("?page=%d&per_page=100", page)
51+
req, err := http.NewRequest("GET", url+params, nil)
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
for key, value := range headers {
57+
req.Header.Set(key, value)
58+
}
59+
60+
client := &http.Client{}
61+
resp, err := client.Do(req)
62+
if err != nil {
63+
return nil, err
64+
}
65+
defer resp.Body.Close()
66+
67+
if resp.StatusCode != 200 {
68+
return nil, fmt.Errorf("unable to fetch repositories from page %d", page)
69+
}
70+
71+
var repos []map[string]interface{}
72+
if err := json.NewDecoder(resp.Body).Decode(&repos); err != nil {
73+
return nil, err
74+
}
75+
if len(repos) == 0 {
76+
break
77+
}
78+
79+
for _, repo := range repos {
80+
repoName := strings.ToLower(repo["name"].(string))
81+
if repoName == currentDirectory {
82+
continue
83+
}
84+
repoPath := repo["html_url"].(string)
85+
if repoPattern == "" || regexp.MustCompile(repoPattern).MatchString(repoName) {
86+
allReposInfo = append(allReposInfo, map[string]string{"name": repoName, "html_url": repoPath})
87+
}
88+
}
89+
page++
90+
}
91+
92+
return allReposInfo, nil
93+
}
94+
95+
func listRepositories(c *cli.Context) error {
96+
organization := c.String("org")
97+
token := c.String("token")
98+
repoPattern := c.String("repo-pattern")
99+
100+
yamlFilesCreated := 0
101+
fmt.Printf("Repositories in %s:\n", organization)
102+
103+
repos, err := getRepositoriesAPI(organization, token, repoPattern)
104+
if err != nil {
105+
return err
106+
}
107+
for _, repo := range repos {
108+
repoName := strings.ToLower(repo["name"])
109+
if repoName == currentDirectory {
110+
continue
111+
}
112+
repoPath := repo["html_url"]
113+
if repoPattern == "" || regexp.MustCompile(repoPattern).MatchString(repoName) {
114+
fmt.Println(repoName)
115+
createOrUpdateCatalogInfo(organization, repoName, repoPath)
116+
yamlFilesCreated++
117+
}
118+
}
119+
fmt.Println("----------")
120+
fmt.Printf("Total YAML files created or updated: %d\n", yamlFilesCreated)
121+
return nil
122+
}
123+
124+
func createOrUpdateCatalogInfo(organization, repoName, repoPath string) {
125+
directory := fmt.Sprintf("services/%s", repoName)
126+
if _, err := os.Stat(directory); os.IsNotExist(err) {
127+
os.MkdirAll(directory, os.ModePerm)
128+
}
129+
130+
yamlFilePath := fmt.Sprintf("%s/catalog-info.yaml", directory)
131+
content := fmt.Sprintf(yamlContentTemplate, repoName, repoPath, fmt.Sprintf("%s/%s", organization, repoName), organization)
132+
133+
if _, err := os.Stat(yamlFilePath); err == nil {
134+
ioutil.WriteFile(yamlFilePath, []byte(content), 0644)
135+
} else {
136+
ioutil.WriteFile(yamlFilePath, []byte(content), 0644)
137+
}
138+
}
139+
140+
func registerYamls(c *cli.Context) error {
141+
organization := c.String("org")
142+
account := c.String("account")
143+
xApiKey := c.String("x-api-key")
144+
145+
fmt.Println("Registering YAML files...")
146+
count := 0
147+
apiURL := fmt.Sprintf("https://idp.harness.io/%s/idp/api/catalog/locations", account)
148+
149+
repos, err := ioutil.ReadDir("./services")
150+
if err != nil {
151+
return err
152+
}
153+
154+
for _, repo := range repos {
155+
if repo.IsDir() && repo.Name() != currentDirectory {
156+
directory := fmt.Sprintf("services/%s", repo.Name())
157+
apiPayload := map[string]string{
158+
"target": fmt.Sprintf("https://github.com/%s/%s/blob/%s/%s/catalog-info.yaml", organization, currentDirectory, branch, directory),
159+
"type": "url",
160+
}
161+
apiHeaders := map[string]string{
162+
"x-api-key": xApiKey,
163+
"Content-Type": "application/json",
164+
"Harness-Account": account,
165+
}
166+
167+
payloadBytes, err := json.Marshal(apiPayload)
168+
if err != nil {
169+
return err
170+
}
171+
172+
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(payloadBytes))
173+
if err != nil {
174+
return err
175+
}
176+
for key, value := range apiHeaders {
177+
req.Header.Set(key, value)
178+
}
179+
180+
client := &http.Client{}
181+
resp, err := client.Do(req)
182+
if err != nil {
183+
return err
184+
}
185+
defer resp.Body.Close()
186+
187+
if resp.StatusCode == 200 || resp.StatusCode == 201 {
188+
fmt.Printf("Location registered for file: %s\n", repo.Name())
189+
count++
190+
} else if resp.StatusCode == 409 {
191+
refreshPayload := map[string]string{"entityRef": fmt.Sprintf("component:default/%s", repo.Name())}
192+
refreshURL := fmt.Sprintf("https://idp.harness.io/%s/idp/api/catalog/refresh", account)
193+
payloadBytes, _ := json.Marshal(refreshPayload)
194+
req, _ := http.NewRequest("POST", refreshURL, bytes.NewBuffer(payloadBytes))
195+
for key, value := range apiHeaders {
196+
req.Header.Set(key, value)
197+
}
198+
resp, _ := client.Do(req)
199+
fmt.Printf("Location already exists for file: %s. Refreshing it\n", repo.Name())
200+
count++
201+
} else {
202+
fmt.Printf("Failed to register location for file: %s. Status code: %d\n", repo.Name(), resp.StatusCode)
203+
}
204+
}
205+
}
206+
return nil
207+
}
208+
209+
func pushYamls() error {
210+
fmt.Println("Pushing YAMLs...")
211+
if err := runCommand("git", "add", "services/"); err != nil {
212+
return err
213+
}
214+
if err := runCommand("git", "commit", "-m", "Adding YAMLs"); err != nil {
215+
return err
216+
}
217+
return runCommand("git", "push")
218+
}
219+
220+
func runCommand(name string, args ...string) error {
221+
cmd := exec.Command(name, args...)
222+
cmd.Stdout = os.Stdout
223+
cmd.Stderr = os.Stderr
224+
return cmd.Run()
225+
}

main.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"harness/account"
66
"harness/auth"
77
"os"
8+
"regexp"
89

910
. "harness/shared"
1011

@@ -66,6 +67,7 @@ func main() {
6667
Destination: &CliCdRequestData.Json,
6768
}),
6869
}
70+
Action :=
6971
app := &cli.App{
7072
Name: "harness",
7173
Version: Version,
@@ -480,6 +482,121 @@ func main() {
480482
},
481483
},
482484
},
485+
{
486+
Name: "internal-developer-portal",
487+
Aliases: []string{"idp"},
488+
Usage: "IDP module specific commands, eg: create, delete, list",
489+
Flags: append(globalFlags,
490+
altsrc.NewStringFlag(&cli.StringFlag{
491+
Name: "file",
492+
Usage: "`YAML` file path for the catalog-entities",
493+
}),
494+
),
495+
Before: func(ctx *cli.Context) error {
496+
auth.HydrateCredsFromPersistence(ctx)
497+
return nil
498+
},
499+
Subcommands: []*cli.Command{
500+
{
501+
Name: "create-catalog-entity",
502+
Usage: "Create a catalog entity using the template YAML",
503+
Flags: []cli.Flag{
504+
&cli.StringFlag{
505+
Name: "yaml-template-name",
506+
Usage: "provide the name to be used for catalog-info yaml",
507+
},
508+
},
509+
Action: func(context *cli.Context) error {
510+
return cliWrapper(createCatalog, context)
511+
},
512+
},
513+
{
514+
Name: "register",
515+
Usage: "Register the YAMLs in your git provider.",
516+
Flags: []cli.Flag{
517+
&cli.StringFlag{
518+
Name: "yaml-template",
519+
Usage: "provide the yaml template path",
520+
},
521+
&cli.StringFlag{
522+
Name: "default",
523+
Usage: "Use the default template",
524+
},
525+
},
526+
Action: func(context *cli.Context) error {
527+
return cliWrapper(createDirectoryYaml, context)
528+
},
529+
},
530+
{
531+
Name: "list",
532+
Usage: "List repositories in a GitHub organization and manage catalog-info.yaml files.",
533+
Flags: []cli.Flag{
534+
&cli.StringFlag{
535+
Name: "org",
536+
Usage: "provide the github organization name",
537+
},
538+
&cli.StringFlag{
539+
Name: "repo-pattern",
540+
Usage: "Optional regex pattern to filter repositories",
541+
},
542+
&cli.StringFlag{
543+
Name: "run-all",
544+
Usage: "Run all operations: create, register, and run",
545+
},
546+
},
547+
},
548+
Action: func(context *cli.Context) error {
549+
org := context.String("org")
550+
repoPattern := context.String("repo-pattern")
551+
runAll := context.Bool("run-all")
552+
553+
if org == "" {
554+
return fmt.Errorf("organization name is required")
555+
}
556+
557+
client := github.NewClient(nil)
558+
559+
opt := &github.RepositoryListByOrgOptions{
560+
ListOptions: github.ListOptions{PerPage: 10},
561+
}
562+
563+
for {
564+
repos, resp, err := client.Repositories.ListByOrg(context.Context, org, opt)
565+
if err != nil {
566+
return fmt.Errorf("error listing repositories: %v", err)
567+
}
568+
569+
for _, repo := range repos {
570+
if repoPattern != "" {
571+
matched, err := regexp.MatchString(repoPattern, *repo.Name)
572+
if err != nil {
573+
return fmt.Errorf("error matching repository pattern: %v", err)
574+
}
575+
if !matched {
576+
continue
577+
}
578+
579+
}
580+
fmt.Printf("Repository: %s\n", *repo.Name)
581+
582+
// Implement your catalog-info.yaml management logic here
583+
584+
if runAll {
585+
fmt.Println("Running all operations for repository:", *repo.Name)
586+
// Implement create, register, and run logic here
587+
}
588+
}
589+
590+
if resp.NextPage == 0 {
591+
break
592+
}
593+
opt.Page = resp.NextPage
594+
}
595+
596+
return nil
597+
},
598+
},
599+
},
483600
{
484601
Name: "infrastructure",
485602
Aliases: []string{"infra"},

0 commit comments

Comments
 (0)