diff --git a/Makefile b/Makefile index 1cb06fb2..6902e548 100644 --- a/Makefile +++ b/Makefile @@ -115,7 +115,7 @@ codegen: $(GOBIN)/mockery pre-commit: lint .PHONY: pre-push -pre-push: lint test codegen check-worktree +pre-push: tidy lint test codegen check-worktree .PHONY: build-docs build-docs: diff --git a/cmd/commands/common.go b/cmd/commands/common.go index 8f7c899c..72b42542 100644 --- a/cmd/commands/common.go +++ b/cmd/commands/common.go @@ -12,6 +12,7 @@ import ( "github.com/argoproj/argocd-autopilot/pkg/util" memfs "github.com/go-git/go-billy/v5/memfs" + billyUtils "github.com/go-git/go-billy/v5/util" "github.com/spf13/cobra" ) @@ -68,8 +69,13 @@ var ( } log.G().Debug("repository is ok") + return r, repofs, nil } + + glob = func(fs fs.FS, pattern string) ([]string, error) { + return billyUtils.Glob(fs, pattern) + } ) func addFlags(cmd *cobra.Command) (*BaseOptions, error) { diff --git a/cmd/commands/project.go b/cmd/commands/project.go index b67a0140..d7fbf6fe 100644 --- a/cmd/commands/project.go +++ b/cmd/commands/project.go @@ -3,8 +3,11 @@ package commands import ( "context" "fmt" + "io" "io/ioutil" + "os" "path/filepath" + "text/tabwriter" "github.com/argoproj/argocd-autopilot/pkg/argocd" "github.com/argoproj/argocd-autopilot/pkg/fs" @@ -46,7 +49,10 @@ func NewProjectCommand() *cobra.Command { }, } + opts, err := addFlags(cmd) + die(err) cmd.AddCommand(NewProjectCreateCommand()) + cmd.AddCommand(NewProjectListCommand(opts)) return cmd } @@ -323,3 +329,97 @@ var getInstallationNamespace = func(repofs fs.FS) (string, error) { return a.Spec.Destination.Namespace, nil } + +type ( + ProjectListOptions struct { + BaseOptions + Out io.Writer + } +) + +func NewProjectListCommand(opts *BaseOptions) *cobra.Command { + + cmd := &cobra.Command{ + Use: "list ", + Short: "Lists all the projects on a git repository", + Example: util.Doc(` +# To run this command you need to create a personal access token for your git provider, +# and have a bootstrapped GitOps repository, and provide them using: + + export GIT_TOKEN= + export GIT_REPO= + +# or with the flags: + + --token --repo + +# Lists projects + + project list +`), + RunE: func(cmd *cobra.Command, args []string) error { + + return RunProjectList(cmd.Context(), &ProjectListOptions{ + BaseOptions: *opts, + Out: os.Stdout, + }) + }, + } + + return cmd +} + +func RunProjectList(ctx context.Context, opts *ProjectListOptions) error { + + _, repofs, err := prepareRepo(ctx, &opts.BaseOptions) + if err != nil { + return err + } + + matches, err := glob(repofs, repofs.Join(store.Default.ProjectsDir, "*.yaml")) + if err != nil { + return err + } + w := tabwriter.NewWriter(opts.Out, 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintf(w, "NAME\tNAMESPACE\tCLUSTER\t\n") + + for _, name := range matches { + proj, _, err := getProjectInfoFromFile(repofs, name) + if err != nil { + return err + } + fmt.Fprintf(w, "%s\t%s\t%s\n", proj.Name, proj.Namespace, proj.ClusterName) + + } + w.Flush() + return nil +} + +var getProjectInfoFromFile = func(fs fs.FS, name string) (*argocdv1alpha1.AppProject, *appsetv1alpha1.ApplicationSpec, error) { + file, err := fs.Open(name) + if err != nil { + return nil, nil, fmt.Errorf("%s not found", name) + } + b, err := ioutil.ReadAll(file) + if err != nil { + return nil, nil, fmt.Errorf("failed to read file %s", name) + } + yamls := util.SplitManifests(b) + if len(yamls) != 2 { + return nil, nil, fmt.Errorf("expected 2 files when splitting %s", name) + } + proj := argocdv1alpha1.AppProject{} + err = yaml.Unmarshal(yamls[0], &proj) + + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal %s", name) + } + appSet := appsetv1alpha1.ApplicationSpec{} + err = yaml.Unmarshal(yamls[1], &proj) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal %s", name) + } + + return &proj, &appSet, nil + +} diff --git a/cmd/commands/project_test.go b/cmd/commands/project_test.go index b15d3987..da78de48 100644 --- a/cmd/commands/project_test.go +++ b/cmd/commands/project_test.go @@ -1,23 +1,29 @@ package commands import ( + "bytes" "context" "fmt" "io" "os" + "reflect" "strings" "testing" + appsetv1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + argocdv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argocd-autopilot/pkg/fs" fsmocks "github.com/argoproj/argocd-autopilot/pkg/fs/mocks" "github.com/argoproj/argocd-autopilot/pkg/git" gitmocks "github.com/argoproj/argocd-autopilot/pkg/git/mocks" "github.com/argoproj/argocd-autopilot/pkg/store" "github.com/argoproj/argocd-autopilot/pkg/util" - + "github.com/ghodss/yaml" + memfs "github.com/go-git/go-billy/v5/memfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestRunProjectCreate(t *testing.T) { @@ -283,3 +289,136 @@ spec: }) } } + +func Test_getProjectInfoFromFile(t *testing.T) { + tests := map[string]struct { + name string + want *argocdv1alpha1.AppProject + wantErr string + beforeFn func(fs.FS) (fs.FS, error) + }{ + "should return error if project file doesn't exist": { + name: "prod.yaml", + wantErr: "prod.yaml not found", + }, + "should failed when 2 files not found": { + name: "prod.yaml", + wantErr: "expected 2 files when splitting prod.yaml", + beforeFn: func(f fs.FS) (fs.FS, error) { + _, err := f.WriteFile("prod.yaml", []byte("content")) + if err != nil { + return nil, err + } + return f, nil + }, + }, + "should return AppProject": { + name: "prod.yaml", + beforeFn: func(f fs.FS) (fs.FS, error) { + appProj := argocdv1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "prod", + Namespace: "ns", + }, + } + appSet := appsetv1alpha1.ApplicationSpec{} + projectYAML, _ := yaml.Marshal(&appProj) + appsetYAML, _ := yaml.Marshal(&appSet) + joinedYAML := util.JoinManifests(projectYAML, appsetYAML) + _, err := f.WriteFile("prod.yaml", joinedYAML) + if err != nil { + return nil, err + } + return f, nil + }, + want: &argocdv1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "prod", + Namespace: "ns", + }, + }, + }, + } + for tName, tt := range tests { + t.Run(tName, func(t *testing.T) { + repofs := fs.Create(memfs.New()) + if tt.beforeFn != nil { + _, err := tt.beforeFn(repofs) + assert.NoError(t, err) + } + got, _, err := getProjectInfoFromFile(repofs, tt.name) + if (err != nil) && tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getProjectInfoFromFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRunProjectList(t *testing.T) { + type args struct { + ctx context.Context + opts *ProjectListOptions + } + tests := map[string]struct { + args args + wantErr bool + prepareRepo func(ctx context.Context, o *BaseOptions) (git.Repository, fs.FS, error) + glob func(fs fs.FS, pattern string) ([]string, error) + getProjectInfoFromFile func(fs fs.FS, name string) (*argocdv1alpha1.AppProject, *appsetv1alpha1.ApplicationSpec, error) + assertFn func(t *testing.T, str string) + }{ + "should print to table": { + args: args{ + opts: &ProjectListOptions{ + BaseOptions: BaseOptions{}, + Out: &bytes.Buffer{}, + }, + }, + glob: func(fs fs.FS, pattern string) ([]string, error) { + res := make([]string, 0, 1) + res = append(res, "prod.yaml") + return res, nil + }, + prepareRepo: func(ctx context.Context, o *BaseOptions) (git.Repository, fs.FS, error) { + memFS := fs.Create(memfs.New()) + return nil, memFS, nil + }, + getProjectInfoFromFile: func(fs fs.FS, name string) (*argocdv1alpha1.AppProject, *appsetv1alpha1.ApplicationSpec, error) { + appProj := &argocdv1alpha1.AppProject{ + ObjectMeta: v1.ObjectMeta{ + Name: "prod", + Namespace: "ns", + }, + } + return appProj, nil, nil + }, + assertFn: func(t *testing.T, str string) { + assert.Contains(t, str, "NAME NAMESPACE CLUSTER \n") + assert.Contains(t, str, "prod ns ") + + }, + }, + } + origPrepareRepo := prepareRepo + origGlob := glob + origGetProjectInfoFromFile := getProjectInfoFromFile + for tName, tt := range tests { + t.Run(tName, func(t *testing.T) { + prepareRepo = tt.prepareRepo + glob = tt.glob + getProjectInfoFromFile = tt.getProjectInfoFromFile + if err := RunProjectList(tt.args.ctx, tt.args.opts); (err != nil) != tt.wantErr { + t.Errorf("RunProjectList() error = %v, wantErr %v", err, tt.wantErr) + } + b := tt.args.opts.Out.(*bytes.Buffer) + tt.assertFn(t, b.String()) + prepareRepo = origPrepareRepo + glob = origGlob + getProjectInfoFromFile = origGetProjectInfoFromFile + }) + } +} diff --git a/docs/commands/argocd-autopilot_project.md b/docs/commands/argocd-autopilot_project.md index e5afe8c2..f7198a0e 100644 --- a/docs/commands/argocd-autopilot_project.md +++ b/docs/commands/argocd-autopilot_project.md @@ -9,7 +9,12 @@ argocd-autopilot project [flags] ### Options ``` - -h, --help help for project + -t, --git-token string Your git provider api token [GIT_TOKEN] + -h, --help help for project + --installation-path string The path where we of the installation files (defaults to the root of the repository [GIT_INSTALLATION_PATH] + -p, --project string Project name + --repo string Repository URL [GIT_REPO] + --revision string Repository branch, tag or commit hash (defaults to HEAD) ``` ### SEE ALSO @@ -17,4 +22,5 @@ argocd-autopilot project [flags] * [argocd-autopilot](argocd-autopilot.md) - argocd-autopilot is used for installing and managing argo-cd installations and argo-cd applications using gitops * [argocd-autopilot project create](argocd-autopilot_project_create.md) - Create a new project +* [argocd-autopilot project list](argocd-autopilot_project_list.md) - Lists all the projects on a git repository diff --git a/docs/commands/argocd-autopilot_project_create.md b/docs/commands/argocd-autopilot_project_create.md index 57e1e5cf..0bea5bae 100644 --- a/docs/commands/argocd-autopilot_project_create.md +++ b/docs/commands/argocd-autopilot_project_create.md @@ -46,20 +46,16 @@ argocd-autopilot project create [PROJECT] [flags] --exec-command-args stringArray Arguments to supply to the --exec-command executable --exec-command-env stringToString Environment vars to set when running the --exec-command executable (default []) --exec-command-install-hint string Text shown to the user when the --exec-command executable doesn't seem to be present - -t, --git-token string Your git provider api token [GIT_TOKEN] --grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. --grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root. -H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers) -h, --help help for create --in-cluster Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc) --insecure Skip server certificate and domain verification - --installation-path string The path where we of the installation files (defaults to the root of the repository [GIT_INSTALLATION_PATH] --name string Overwrite the cluster name --plaintext Disable TLS --port-forward Connect to a random argocd-server port using port forwarding --port-forward-namespace string Namespace name which should be used for port forwarding - --repo string Repository URL [GIT_REPO] - --revision string Repository branch, tag or commit hash (defaults to HEAD) --server string Argo CD server address --server-crt string Server certificate file --service-account string System namespace service account to use for kubernetes resource management. If not set then default "argocd-manager" SA will be created @@ -68,6 +64,16 @@ argocd-autopilot project create [PROJECT] [flags] --upsert Override an existing cluster with the same name even if the spec differs ``` +### Options inherited from parent commands + +``` + -t, --git-token string Your git provider api token [GIT_TOKEN] + --installation-path string The path where we of the installation files (defaults to the root of the repository [GIT_INSTALLATION_PATH] + -p, --project string Project name + --repo string Repository URL [GIT_REPO] + --revision string Repository branch, tag or commit hash (defaults to HEAD) +``` + ### SEE ALSO * [argocd-autopilot project](argocd-autopilot_project.md) - Manage projects diff --git a/docs/commands/argocd-autopilot_project_list.md b/docs/commands/argocd-autopilot_project_list.md new file mode 100644 index 00000000..e2f44d2f --- /dev/null +++ b/docs/commands/argocd-autopilot_project_list.md @@ -0,0 +1,48 @@ +## argocd-autopilot project list + +Lists all the projects on a git repository + +``` +argocd-autopilot project list [flags] +``` + +### Examples + +``` + +# To run this command you need to create a personal access token for your git provider, +# and have a bootstrapped GitOps repository, and provide them using: + + export GIT_TOKEN= + export GIT_REPO= + +# or with the flags: + + --token --repo + +# Lists projects + + argocd-autopilot project list + +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + -t, --git-token string Your git provider api token [GIT_TOKEN] + --installation-path string The path where we of the installation files (defaults to the root of the repository [GIT_INSTALLATION_PATH] + -p, --project string Project name + --repo string Repository URL [GIT_REPO] + --revision string Repository branch, tag or commit hash (defaults to HEAD) +``` + +### SEE ALSO + +* [argocd-autopilot project](argocd-autopilot_project.md) - Manage projects + diff --git a/pkg/util/util.go b/pkg/util/util.go index 55b260d1..77d35a8e 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -135,6 +135,16 @@ func JoinManifests(manifests ...[]byte) []byte { return []byte(strings.Join(res, yamlSeperator)) } +func SplitManifests(manifests []byte) [][]byte { + str := string(manifests) + stringManifests := strings.Split(str, yamlSeperator) + res := make([][]byte, 0, len(stringManifests)) + for _, m := range stringManifests { + res = append(res, []byte(m)) + } + return res +} + func StealFlags(cmd *cobra.Command, exceptFor []string) (*pflag.FlagSet, error) { fs := &pflag.FlagSet{} ef := map[string]bool{}