From 3a05fe1c37ffb84edf284f0d381a384dd3d8158e Mon Sep 17 00:00:00 2001 From: rumstead Date: Tue, 21 Dec 2021 10:03:00 -0500 Subject: [PATCH 1/8] feat: #94 adding skeleton code for ado provider --- docs/Getting-Started.md | 2 +- .../argocd-autopilot_repo_bootstrap.md | 2 +- go.mod | 2 + go.sum | 5 +- pkg/git/provider.go | 9 ++- pkg/git/provider_ado.go | 50 +++++++++++++++ pkg/git/provider_ado_test.go | 62 +++++++++++++++++++ pkg/git/provider_gitea.go | 2 +- pkg/git/provider_gitlab.go | 2 +- pkg/git/repository_test.go | 2 +- 10 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 pkg/git/provider_ado.go create mode 100644 pkg/git/provider_ado_test.go diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 08d5a1d7..de22ac06 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -36,7 +36,7 @@ export GIT_REPO=https://github.com/owner/name?ref=gitops_branch #### Using a Specific git Provider You can add the `--provider` flag to the `repo bootstrap` command, to enforce using a specific provider when creating a new repository. If the value is not supplied, the code will attempt to infer it from the clone URL. -Autopilot currently support github, gitlab and gitea as SCM providers. +Autopilot currently support github, gitlab, azure devops, and gitea as SCM providers. All the following commands will use the variables you supplied in order to manage your GitOps repository. diff --git a/docs/commands/argocd-autopilot_repo_bootstrap.md b/docs/commands/argocd-autopilot_repo_bootstrap.md index 18612d3a..55be86ce 100644 --- a/docs/commands/argocd-autopilot_repo_bootstrap.md +++ b/docs/commands/argocd-autopilot_repo_bootstrap.md @@ -45,7 +45,7 @@ argocd-autopilot repo bootstrap [flags] --installation-mode string One of: normal|flat. If flat, will commit the bootstrap manifests, otherwise will commit the bootstrap kustomization.yaml (default "normal") --kubeconfig string Path to the kubeconfig file to use for CLI requests. -n, --namespace string If present, the namespace scope for this CLI request - --provider string The git provider, one of: gitea|github|gitlab + --provider string The git provider, one of: ado|gitea|github|gitlab --repo string Repository URL [GIT_REPO] --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") ``` diff --git a/go.mod b/go.mod index 5539feba..df375681 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,8 @@ require ( github.com/go-git/go-git/v5 v5.4.1 github.com/gobuffalo/packr v1.30.1 github.com/google/go-github/v35 v35.3.0 + github.com/google/uuid v1.3.0 // indirect + github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index 2a0e4bfe..c5ce4081 100644 --- a/go.sum +++ b/go.sum @@ -442,8 +442,9 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= @@ -615,6 +616,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= diff --git a/pkg/git/provider.go b/pkg/git/provider.go index 8d93256d..28116d9f 100644 --- a/pkg/git/provider.go +++ b/pkg/git/provider.go @@ -29,8 +29,10 @@ type ( } CreateRepoOptions struct { - Owner string - Name string + Owner string + Name string + // ADO project name + Project string Private bool } @@ -54,6 +56,7 @@ var supportedProviders = map[string]func(*ProviderOptions) (Provider, error){ "github": newGithub, "gitea": newGitea, "gitlab": newGitlab, + "ado": newAdo, } // New creates a new git provider @@ -72,6 +75,6 @@ func Providers() []string { res = append(res, p) } - sort.Strings(res) // must sort the providers by name, otherwise the codegen is not determenistic + sort.Strings(res) // must sort the providers by name, otherwise the codegen is not deterministic return res } diff --git a/pkg/git/provider_ado.go b/pkg/git/provider_ado.go new file mode 100644 index 00000000..9038f398 --- /dev/null +++ b/pkg/git/provider_ado.go @@ -0,0 +1,50 @@ +package git + +import ( + "context" + "github.com/microsoft/azure-devops-go-api/azuredevops" + ado "github.com/microsoft/azure-devops-go-api/azuredevops/git" + "time" +) + +//go:generate mockery --name AdoClient --output ado/mocks --case snake +type ( + AdoClient interface { + CreateRepository(context.Context, ado.CreateRepositoryArgs) (*ado.GitRepository, error) + } + adoGit struct { + adoClient AdoClient + } +) + +const timeoutTime = 10 * time.Second + +func newAdo(opts *ProviderOptions) (Provider, error) { + connection := azuredevops.NewPatConnection(opts.Host, opts.Auth.Password) + ctx, cancel := context.WithTimeout(context.Background(), timeoutTime) + defer cancel() + // FYI: ado also has a "core" client that can be used to update project, teams, and other ADO constructs + gitClient, err := ado.NewClient(ctx, connection) + if err != nil { + return nil, err + } + + return &adoGit{ + adoClient: gitClient, + }, nil +} + +func (g *adoGit) CreateRepository(ctx context.Context, opts *CreateRepoOptions) (string, error) { + gitRepoToCreate := &ado.GitRepositoryCreateOptions{ + Name: &opts.Name, + } + createRepositoryArgs := ado.CreateRepositoryArgs{ + GitRepositoryToCreate: gitRepoToCreate, + Project: &opts.Project, + } + repository, err := g.adoClient.CreateRepository(ctx, createRepositoryArgs) + if err != nil { + return "", err + } + return *repository.Url, nil +} diff --git a/pkg/git/provider_ado_test.go b/pkg/git/provider_ado_test.go new file mode 100644 index 00000000..3c9175b2 --- /dev/null +++ b/pkg/git/provider_ado_test.go @@ -0,0 +1,62 @@ +package git + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_adoGit_CreateRepository(t *testing.T) { + type fields struct { + adoClient AdoClient + } + type args struct { + ctx context.Context + opts *CreateRepoOptions + } + tests := []struct { + name string + fields fields + args args + want string + wantErr assert.ErrorAssertionFunc + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &adoGit{ + adoClient: tt.fields.adoClient, + } + got, err := g.CreateRepository(tt.args.ctx, tt.args.opts) + if !tt.wantErr(t, err, fmt.Sprintf("CreateRepository(%v, %v)", tt.args.ctx, tt.args.opts)) { + return + } + assert.Equalf(t, tt.want, got, "CreateRepository(%v, %v)", tt.args.ctx, tt.args.opts) + }) + } +} + +func Test_newAdo(t *testing.T) { + type args struct { + opts *ProviderOptions + } + tests := []struct { + name string + args args + want Provider + wantErr assert.ErrorAssertionFunc + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newAdo(tt.args.opts) + if !tt.wantErr(t, err, fmt.Sprintf("newAdo(%v)", tt.args.opts)) { + return + } + assert.Equalf(t, tt.want, got, "newAdo(%v)", tt.args.opts) + }) + } +} diff --git a/pkg/git/provider_gitea.go b/pkg/git/provider_gitea.go index 63459661..b87e33f9 100644 --- a/pkg/git/provider_gitea.go +++ b/pkg/git/provider_gitea.go @@ -34,7 +34,7 @@ func newGitea(opts *ProviderOptions) (Provider, error) { return g, nil } -func (g *gitea) CreateRepository(ctx context.Context, opts *CreateRepoOptions) (string, error) { +func (g *gitea) CreateRepository(_ context.Context, opts *CreateRepoOptions) (string, error) { authUser, res, err := g.client.GetMyUserInfo() if err != nil { if res.StatusCode == 401 { diff --git a/pkg/git/provider_gitlab.go b/pkg/git/provider_gitlab.go index 40ebbd67..1ce0a68b 100644 --- a/pkg/git/provider_gitlab.go +++ b/pkg/git/provider_gitlab.go @@ -46,7 +46,7 @@ func newGitlab(opts *ProviderOptions) (Provider, error) { return g, nil } -func (g *gitlab) CreateRepository(ctx context.Context, opts *CreateRepoOptions) (string, error) { +func (g *gitlab) CreateRepository(_ context.Context, opts *CreateRepoOptions) (string, error) { authUser, res, err := g.client.CurrentUser() if err != nil { if res.StatusCode == 401 { diff --git a/pkg/git/repository_test.go b/pkg/git/repository_test.go index fcfe2ef7..ec7f4eb5 100644 --- a/pkg/git/repository_test.go +++ b/pkg/git/repository_test.go @@ -882,7 +882,7 @@ func TestAddFlags(t *testing.T) { wantedFlags: []flag{ { name: "provider", - usage: "The git provider, one of: gitea|github|gitlab", + usage: "The git provider, one of: gitea|github|gitlab|ado", }, }, }, From b4d5ae25baa39c3d496e9d0e5604bcf78e52a72a Mon Sep 17 00:00:00 2001 From: rumstead Date: Tue, 21 Dec 2021 12:21:22 -0500 Subject: [PATCH 2/8] feat: adding tests --- pkg/git/ado/mocks/ado_client.go | 38 ++++++++++++ pkg/git/provider_ado.go | 7 ++- pkg/git/provider_ado_test.go | 105 ++++++++++++++++++++++---------- pkg/git/repository_test.go | 2 +- 4 files changed, 117 insertions(+), 35 deletions(-) create mode 100644 pkg/git/ado/mocks/ado_client.go diff --git a/pkg/git/ado/mocks/ado_client.go b/pkg/git/ado/mocks/ado_client.go new file mode 100644 index 00000000..0ae56874 --- /dev/null +++ b/pkg/git/ado/mocks/ado_client.go @@ -0,0 +1,38 @@ +// Code generated by mockery (devel). DO NOT EDIT. + +package mocks + +import ( + context "context" + + git "github.com/microsoft/azure-devops-go-api/azuredevops/git" + mock "github.com/stretchr/testify/mock" +) + +// AdoClient is an autogenerated mock type for the AdoClient type +type AdoClient struct { + mock.Mock +} + +// CreateRepository provides a mock function with given fields: _a0, _a1 +func (_m *AdoClient) CreateRepository(_a0 context.Context, _a1 git.CreateRepositoryArgs) (*git.GitRepository, error) { + ret := _m.Called(_a0, _a1) + + var r0 *git.GitRepository + if rf, ok := ret.Get(0).(func(context.Context, git.CreateRepositoryArgs) *git.GitRepository); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*git.GitRepository) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, git.CreateRepositoryArgs) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/git/provider_ado.go b/pkg/git/provider_ado.go index 9038f398..effe026c 100644 --- a/pkg/git/provider_ado.go +++ b/pkg/git/provider_ado.go @@ -2,6 +2,7 @@ package git import ( "context" + "fmt" "github.com/microsoft/azure-devops-go-api/azuredevops" ado "github.com/microsoft/azure-devops-go-api/azuredevops/git" "time" @@ -35,6 +36,10 @@ func newAdo(opts *ProviderOptions) (Provider, error) { } func (g *adoGit) CreateRepository(ctx context.Context, opts *CreateRepoOptions) (string, error) { + if opts.Name == "" || opts.Project == "" { + return "", fmt.Errorf("name and project need to be provided to create an azure devops repository. "+ + "name: '%s' project '%s'", opts.Name, opts.Project) + } gitRepoToCreate := &ado.GitRepositoryCreateOptions{ Name: &opts.Name, } @@ -46,5 +51,5 @@ func (g *adoGit) CreateRepository(ctx context.Context, opts *CreateRepoOptions) if err != nil { return "", err } - return *repository.Url, nil + return *repository.RemoteUrl, nil } diff --git a/pkg/git/provider_ado_test.go b/pkg/git/provider_ado_test.go index 3c9175b2..0b31e87e 100644 --- a/pkg/git/provider_ado_test.go +++ b/pkg/git/provider_ado_test.go @@ -2,32 +2,94 @@ package git import ( "context" + "errors" "fmt" + adoMock "github.com/argoproj-labs/argocd-autopilot/pkg/git/ado/mocks" + ado "github.com/microsoft/azure-devops-go-api/azuredevops/git" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "testing" ) func Test_adoGit_CreateRepository(t *testing.T) { - type fields struct { - adoClient AdoClient - } + remoteURL := "https://dev.azure.com/SUB/PROJECT/_git/REPO" + emptyFunc := func(client *adoMock.AdoClient) {} type args struct { ctx context.Context opts *CreateRepoOptions } tests := []struct { - name string - fields fields - args args - want string - wantErr assert.ErrorAssertionFunc + name string + mockClient func(client *adoMock.AdoClient) + args args + want string + wantErr assert.ErrorAssertionFunc }{ - // TODO: Add test cases. + {name: "Empty Name", mockClient: emptyFunc, args: args{ + ctx: context.TODO(), + opts: &CreateRepoOptions{ + Owner: "rumstead", + Name: "", + Project: "project", + }, + }, want: "", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return true + }}, + {name: "Empty Project", mockClient: emptyFunc, args: args{ + ctx: context.TODO(), + opts: &CreateRepoOptions{ + Owner: "rumstead", + Name: "name", + Project: "", + }, + }, want: "", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return true + }}, + {name: "Failure creating repo", mockClient: func(client *adoMock.AdoClient) { + client.On("CreateRepository", context.TODO(), mock.AnythingOfType("CreateRepositoryArgs")).Return(nil, errors.New("ah an error")) + }, args: args{ + ctx: context.TODO(), + opts: &CreateRepoOptions{ + Owner: "rumstead", + Name: "name", + Project: "project", + }, + }, want: "", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return true + }}, + {name: "Success creating repo", mockClient: func(client *adoMock.AdoClient) { + client.On("CreateRepository", context.TODO(), mock.AnythingOfType("CreateRepositoryArgs")).Return(&ado.GitRepository{ + Links: nil, + DefaultBranch: nil, + Id: nil, + IsFork: nil, + Name: nil, + ParentRepository: nil, + Project: nil, + RemoteUrl: &remoteURL, + Size: nil, + SshUrl: nil, + Url: nil, + ValidRemoteUrls: nil, + WebUrl: nil, + }, nil) + }, args: args{ + ctx: context.TODO(), + opts: &CreateRepoOptions{ + Owner: "rumstead", + Name: "name", + Project: "project", + }, + }, want: "https://dev.azure.com/SUB/PROJECT/_git/REPO", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return false + }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + mockClient := &adoMock.AdoClient{} + tt.mockClient(mockClient) g := &adoGit{ - adoClient: tt.fields.adoClient, + adoClient: mockClient, } got, err := g.CreateRepository(tt.args.ctx, tt.args.opts) if !tt.wantErr(t, err, fmt.Sprintf("CreateRepository(%v, %v)", tt.args.ctx, tt.args.opts)) { @@ -37,26 +99,3 @@ func Test_adoGit_CreateRepository(t *testing.T) { }) } } - -func Test_newAdo(t *testing.T) { - type args struct { - opts *ProviderOptions - } - tests := []struct { - name string - args args - want Provider - wantErr assert.ErrorAssertionFunc - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := newAdo(tt.args.opts) - if !tt.wantErr(t, err, fmt.Sprintf("newAdo(%v)", tt.args.opts)) { - return - } - assert.Equalf(t, tt.want, got, "newAdo(%v)", tt.args.opts) - }) - } -} diff --git a/pkg/git/repository_test.go b/pkg/git/repository_test.go index ec7f4eb5..f0f9a6ef 100644 --- a/pkg/git/repository_test.go +++ b/pkg/git/repository_test.go @@ -882,7 +882,7 @@ func TestAddFlags(t *testing.T) { wantedFlags: []flag{ { name: "provider", - usage: "The git provider, one of: gitea|github|gitlab|ado", + usage: "The git provider, one of: ado|gitea|github|gitlab", }, }, }, From 4ebb2b2c97b3e73c700880baf5ff734936954c65 Mon Sep 17 00:00:00 2001 From: rumstead Date: Tue, 21 Dec 2021 15:57:02 -0500 Subject: [PATCH 3/8] feat: adding tests --- pkg/git/ado/mocks/ado_client.go | 38 ------------------ pkg/git/provider.go | 8 ++-- pkg/git/provider_ado.go | 69 ++++++++++++++++++++++++++++++--- pkg/git/provider_ado_test.go | 43 +++++++++----------- pkg/git/repository.go | 28 +++++++++++-- pkg/git/repository_test.go | 2 +- 6 files changed, 109 insertions(+), 79 deletions(-) delete mode 100644 pkg/git/ado/mocks/ado_client.go diff --git a/pkg/git/ado/mocks/ado_client.go b/pkg/git/ado/mocks/ado_client.go deleted file mode 100644 index 0ae56874..00000000 --- a/pkg/git/ado/mocks/ado_client.go +++ /dev/null @@ -1,38 +0,0 @@ -// Code generated by mockery (devel). DO NOT EDIT. - -package mocks - -import ( - context "context" - - git "github.com/microsoft/azure-devops-go-api/azuredevops/git" - mock "github.com/stretchr/testify/mock" -) - -// AdoClient is an autogenerated mock type for the AdoClient type -type AdoClient struct { - mock.Mock -} - -// CreateRepository provides a mock function with given fields: _a0, _a1 -func (_m *AdoClient) CreateRepository(_a0 context.Context, _a1 git.CreateRepositoryArgs) (*git.GitRepository, error) { - ret := _m.Called(_a0, _a1) - - var r0 *git.GitRepository - if rf, ok := ret.Get(0).(func(context.Context, git.CreateRepositoryArgs) *git.GitRepository); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*git.GitRepository) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, git.CreateRepositoryArgs) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} diff --git a/pkg/git/provider.go b/pkg/git/provider.go index 28116d9f..6db61497 100644 --- a/pkg/git/provider.go +++ b/pkg/git/provider.go @@ -29,10 +29,8 @@ type ( } CreateRepoOptions struct { - Owner string - Name string - // ADO project name - Project string + Owner string + Name string Private bool } @@ -56,7 +54,7 @@ var supportedProviders = map[string]func(*ProviderOptions) (Provider, error){ "github": newGithub, "gitea": newGitea, "gitlab": newGitlab, - "ado": newAdo, + Azure: newAdo, } // New creates a new git provider diff --git a/pkg/git/provider_ado.go b/pkg/git/provider_ado.go index effe026c..2f6204c6 100644 --- a/pkg/git/provider_ado.go +++ b/pkg/git/provider_ado.go @@ -5,23 +5,45 @@ import ( "fmt" "github.com/microsoft/azure-devops-go-api/azuredevops" ado "github.com/microsoft/azure-devops-go-api/azuredevops/git" + "net/url" + "strings" "time" ) -//go:generate mockery --name AdoClient --output ado/mocks --case snake +//go:generate mockery --name Ado* --output ado/mocks --case snake type ( AdoClient interface { CreateRepository(context.Context, ado.CreateRepositoryArgs) (*ado.GitRepository, error) } + + AdoUrl interface { + GetProjectName() string + } + adoGit struct { adoClient AdoClient + opts *ProviderOptions + adoUrl AdoUrl + } + + adoGitUrl struct { + loginUrl string + subscription string + projectName string + repoName string } ) +const Azure = "azure" +const AzureHostName = "dev.azure" const timeoutTime = 10 * time.Second func newAdo(opts *ProviderOptions) (Provider, error) { - connection := azuredevops.NewPatConnection(opts.Host, opts.Auth.Password) + adoUrl, err := parseAdoUrl(opts.Host) + if err != nil { + return nil, err + } + connection := azuredevops.NewPatConnection(adoUrl.loginUrl, opts.Auth.Password) ctx, cancel := context.WithTimeout(context.Background(), timeoutTime) defer cancel() // FYI: ado also has a "core" client that can be used to update project, teams, and other ADO constructs @@ -32,20 +54,23 @@ func newAdo(opts *ProviderOptions) (Provider, error) { return &adoGit{ adoClient: gitClient, + opts: opts, + adoUrl: adoUrl, }, nil } func (g *adoGit) CreateRepository(ctx context.Context, opts *CreateRepoOptions) (string, error) { - if opts.Name == "" || opts.Project == "" { - return "", fmt.Errorf("name and project need to be provided to create an azure devops repository. "+ - "name: '%s' project '%s'", opts.Name, opts.Project) + if opts.Name == "" { + return "", fmt.Errorf("name needs to be provided to create an azure devops repository. "+ + "name: '%s'", opts.Name) } gitRepoToCreate := &ado.GitRepositoryCreateOptions{ Name: &opts.Name, } + project := g.adoUrl.GetProjectName() createRepositoryArgs := ado.CreateRepositoryArgs{ GitRepositoryToCreate: gitRepoToCreate, - Project: &opts.Project, + Project: &project, } repository, err := g.adoClient.CreateRepository(ctx, createRepositoryArgs) if err != nil { @@ -53,3 +78,35 @@ func (g *adoGit) CreateRepository(ctx context.Context, opts *CreateRepoOptions) } return *repository.RemoteUrl, nil } + +func (a *adoGitUrl) GetProjectName() string { + return a.projectName +} + +// getLoginUrl parses a URL and returns the URL with only first part of the path +// this path should be the subscription in Azure DevOps +// ie, https://dev.azure.com/SUB/PROJECT/_git/REPO would return https://dev.azure.com/SUB +func parseAdoUrl(host string) (*adoGitUrl, error) { + u, err := url.Parse(host) + if err != nil { + return nil, err + } + var sub, project, repoName string + path := strings.Split(u.Path, "/") + if len(path) < 5 { + return nil, fmt.Errorf("unable to parse Azure DevOps url") + } else { + // 1 since the path starts with a slash + sub = path[1] + project = path[2] + // skip _git + repoName = path[4] + } + loginUrl := fmt.Sprintf("%s://%s/%s", u.Scheme, u.Host, sub) + return &adoGitUrl{ + loginUrl: loginUrl, + subscription: sub, + projectName: project, + repoName: repoName, + }, nil +} diff --git a/pkg/git/provider_ado_test.go b/pkg/git/provider_ado_test.go index 0b31e87e..4404bacd 100644 --- a/pkg/git/provider_ado_test.go +++ b/pkg/git/provider_ado_test.go @@ -13,14 +13,14 @@ import ( func Test_adoGit_CreateRepository(t *testing.T) { remoteURL := "https://dev.azure.com/SUB/PROJECT/_git/REPO" - emptyFunc := func(client *adoMock.AdoClient) {} + emptyFunc := func(client *adoMock.AdoClient, url *adoMock.AdoUrl) {} type args struct { ctx context.Context opts *CreateRepoOptions } tests := []struct { name string - mockClient func(client *adoMock.AdoClient) + mockClient func(client *adoMock.AdoClient, url *adoMock.AdoUrl) args args want string wantErr assert.ErrorAssertionFunc @@ -28,36 +28,28 @@ func Test_adoGit_CreateRepository(t *testing.T) { {name: "Empty Name", mockClient: emptyFunc, args: args{ ctx: context.TODO(), opts: &CreateRepoOptions{ - Owner: "rumstead", - Name: "", - Project: "project", + Owner: "rumstead", + Name: "", }, }, want: "", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { return true }}, - {name: "Empty Project", mockClient: emptyFunc, args: args{ - ctx: context.TODO(), - opts: &CreateRepoOptions{ - Owner: "rumstead", - Name: "name", - Project: "", - }, - }, want: "", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return true - }}, - {name: "Failure creating repo", mockClient: func(client *adoMock.AdoClient) { - client.On("CreateRepository", context.TODO(), mock.AnythingOfType("CreateRepositoryArgs")).Return(nil, errors.New("ah an error")) + {name: "Failure creating repo", mockClient: func(client *adoMock.AdoClient, url *adoMock.AdoUrl) { + client.On("CreateRepository", context.TODO(), + mock.AnythingOfType("CreateRepositoryArgs")). + Return(nil, errors.New("ah an error")) + url.On("GetProjectName").Return("blah") }, args: args{ ctx: context.TODO(), opts: &CreateRepoOptions{ - Owner: "rumstead", - Name: "name", - Project: "project", + Owner: "rumstead", + Name: "name", }, }, want: "", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { return true }}, - {name: "Success creating repo", mockClient: func(client *adoMock.AdoClient) { + {name: "Success creating repo", mockClient: func(client *adoMock.AdoClient, url *adoMock.AdoUrl) { + url.On("GetProjectName").Return("PROJECT") client.On("CreateRepository", context.TODO(), mock.AnythingOfType("CreateRepositoryArgs")).Return(&ado.GitRepository{ Links: nil, DefaultBranch: nil, @@ -76,9 +68,8 @@ func Test_adoGit_CreateRepository(t *testing.T) { }, args: args{ ctx: context.TODO(), opts: &CreateRepoOptions{ - Owner: "rumstead", - Name: "name", - Project: "project", + Owner: "rumstead", + Name: "name", }, }, want: "https://dev.azure.com/SUB/PROJECT/_git/REPO", wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { return false @@ -87,9 +78,11 @@ func Test_adoGit_CreateRepository(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockClient := &adoMock.AdoClient{} - tt.mockClient(mockClient) + mockUrl := &adoMock.AdoUrl{} + tt.mockClient(mockClient, mockUrl) g := &adoGit{ adoClient: mockClient, + adoUrl: mockUrl, } got, err := g.CreateRepository(tt.args.ctx, tt.args.opts) if !tt.wantErr(t, err, fmt.Sprintf("CreateRepository(%v, %v)", tt.args.ctx, tt.args.opts)) { diff --git a/pkg/git/repository.go b/pkg/git/repository.go index 01d3aefc..648e7fec 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -372,7 +372,11 @@ var createRepo = func(ctx context.Context, opts *CloneOptions) (string, error) { return "", err } - providerType = strings.TrimSuffix(u.Hostname(), ".com") + if strings.Contains(u.Hostname(), AzureHostName) { + providerType = Azure + } else { + providerType = strings.TrimSuffix(u.Hostname(), ".com") + } log.G(ctx).Warnf("--provider not specified, assuming provider from url: %s", providerType) } @@ -385,18 +389,34 @@ var createRepo = func(ctx context.Context, opts *CloneOptions) (string, error) { return "", fmt.Errorf("failed to create the repository, you can try to manually create it before trying again: %w", err) } + switch providerType { + case Azure: + return p.CreateRepository(ctx, &CreateRepoOptions{ + Owner: "", + Name: orgRepo, + }) + default: + repoOptions, err := getDefaultRepoOptions(orgRepo) + if err != nil { + return "", nil + } + return p.CreateRepository(ctx, repoOptions) + } +} + +func getDefaultRepoOptions(orgRepo string) (*CreateRepoOptions, error) { s := strings.Split(orgRepo, "/") if len(s) < 2 { - return "", fmt.Errorf("failed parsing organization and repo from '%s'", orgRepo) + return nil, fmt.Errorf("failed parsing organization and repo from '%s'", orgRepo) } owner := strings.Join(s[:len(s)-1], "/") name := s[len(s)-1] - return p.CreateRepository(ctx, &CreateRepoOptions{ + return &CreateRepoOptions{ Owner: owner, Name: name, Private: true, - }) + }, nil } var initRepo = func(ctx context.Context, opts *CloneOptions) (*repo, error) { diff --git a/pkg/git/repository_test.go b/pkg/git/repository_test.go index f0f9a6ef..b4ff260c 100644 --- a/pkg/git/repository_test.go +++ b/pkg/git/repository_test.go @@ -882,7 +882,7 @@ func TestAddFlags(t *testing.T) { wantedFlags: []flag{ { name: "provider", - usage: "The git provider, one of: ado|gitea|github|gitlab", + usage: "The git provider, one of: azure|gitea|github|gitlab", }, }, }, From 42def4ff24200e0ac52c2a07e84ee10713a55ab5 Mon Sep 17 00:00:00 2001 From: rumstead Date: Tue, 21 Dec 2021 15:57:20 -0500 Subject: [PATCH 4/8] feat: adding tests --- pkg/git/ado/mocks/ado_client.go | 38 +++++++++++++++++++++++++++++++++ pkg/git/ado/mocks/ado_url.go | 24 +++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 pkg/git/ado/mocks/ado_client.go create mode 100644 pkg/git/ado/mocks/ado_url.go diff --git a/pkg/git/ado/mocks/ado_client.go b/pkg/git/ado/mocks/ado_client.go new file mode 100644 index 00000000..0ae56874 --- /dev/null +++ b/pkg/git/ado/mocks/ado_client.go @@ -0,0 +1,38 @@ +// Code generated by mockery (devel). DO NOT EDIT. + +package mocks + +import ( + context "context" + + git "github.com/microsoft/azure-devops-go-api/azuredevops/git" + mock "github.com/stretchr/testify/mock" +) + +// AdoClient is an autogenerated mock type for the AdoClient type +type AdoClient struct { + mock.Mock +} + +// CreateRepository provides a mock function with given fields: _a0, _a1 +func (_m *AdoClient) CreateRepository(_a0 context.Context, _a1 git.CreateRepositoryArgs) (*git.GitRepository, error) { + ret := _m.Called(_a0, _a1) + + var r0 *git.GitRepository + if rf, ok := ret.Get(0).(func(context.Context, git.CreateRepositoryArgs) *git.GitRepository); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*git.GitRepository) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, git.CreateRepositoryArgs) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/git/ado/mocks/ado_url.go b/pkg/git/ado/mocks/ado_url.go new file mode 100644 index 00000000..8da85743 --- /dev/null +++ b/pkg/git/ado/mocks/ado_url.go @@ -0,0 +1,24 @@ +// Code generated by mockery (devel). DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// AdoUrl is an autogenerated mock type for the AdoUrl type +type AdoUrl struct { + mock.Mock +} + +// GetProjectName provides a mock function with given fields: +func (_m *AdoUrl) GetProjectName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} From 6cfb442efd90ee64e36638789f856de3eec1c1de Mon Sep 17 00:00:00 2001 From: rumstead Date: Tue, 21 Dec 2021 19:07:19 -0500 Subject: [PATCH 5/8] rerun code gen --- docs/commands/argocd-autopilot_repo_bootstrap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands/argocd-autopilot_repo_bootstrap.md b/docs/commands/argocd-autopilot_repo_bootstrap.md index 55be86ce..e3919acd 100644 --- a/docs/commands/argocd-autopilot_repo_bootstrap.md +++ b/docs/commands/argocd-autopilot_repo_bootstrap.md @@ -45,7 +45,7 @@ argocd-autopilot repo bootstrap [flags] --installation-mode string One of: normal|flat. If flat, will commit the bootstrap manifests, otherwise will commit the bootstrap kustomization.yaml (default "normal") --kubeconfig string Path to the kubeconfig file to use for CLI requests. -n, --namespace string If present, the namespace scope for this CLI request - --provider string The git provider, one of: ado|gitea|github|gitlab + --provider string The git provider, one of: azure|gitea|github|gitlab --repo string Repository URL [GIT_REPO] --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") ``` From bcf5c4511b2df96c32d8b5b99aef0049bf15fece Mon Sep 17 00:00:00 2001 From: rumstead Date: Tue, 21 Dec 2021 19:48:05 -0500 Subject: [PATCH 6/8] adding another test --- pkg/git/provider_ado_test.go | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pkg/git/provider_ado_test.go b/pkg/git/provider_ado_test.go index 4404bacd..63cd089b 100644 --- a/pkg/git/provider_ado_test.go +++ b/pkg/git/provider_ado_test.go @@ -92,3 +92,41 @@ func Test_adoGit_CreateRepository(t *testing.T) { }) } } + +func Test_parseAdoUrl(t *testing.T) { + type args struct { + host string + } + tests := []struct { + name string + args args + want *adoGitUrl + wantErr assert.ErrorAssertionFunc + }{ + {name: "Invalid URL", args: args{host: "https://dev.azure.com"}, want: nil, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return true + }}, + // url taking from the url_test in the url/net module + {name: "Parse Error", args: args{host: "http://[fe80::%31]:8080/"}, want: nil, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return true + }}, + {name: "Parse URL", args: args{host: "https://dev.azure.com/SUB/PROJECT/_git/REPO "}, want: &adoGitUrl{ + loginUrl: "https://dev.azure.com/SUB", + subscription: "SUB", + projectName: "PROJECT", + repoName: "REPO", + }, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return false + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseAdoUrl(tt.args.host) + if !tt.wantErr(t, err, fmt.Sprintf("parseAdoUrl(%v)", tt.args.host)) { + return + } + assert.Equalf(t, tt.want, got, "parseAdoUrl(%v)", tt.args.host) + }) + } +} From 753e724d973a51199be0e8bff53a9f07daaede26 Mon Sep 17 00:00:00 2001 From: rumstead Date: Wed, 22 Dec 2021 10:42:15 -0500 Subject: [PATCH 7/8] removing unneeded and confusing url parsing --- pkg/git/provider_ado.go | 6 +----- pkg/git/provider_ado_test.go | 8 +++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pkg/git/provider_ado.go b/pkg/git/provider_ado.go index 2f6204c6..1211d03a 100644 --- a/pkg/git/provider_ado.go +++ b/pkg/git/provider_ado.go @@ -30,7 +30,6 @@ type ( loginUrl string subscription string projectName string - repoName string } ) @@ -91,7 +90,7 @@ func parseAdoUrl(host string) (*adoGitUrl, error) { if err != nil { return nil, err } - var sub, project, repoName string + var sub, project string path := strings.Split(u.Path, "/") if len(path) < 5 { return nil, fmt.Errorf("unable to parse Azure DevOps url") @@ -99,14 +98,11 @@ func parseAdoUrl(host string) (*adoGitUrl, error) { // 1 since the path starts with a slash sub = path[1] project = path[2] - // skip _git - repoName = path[4] } loginUrl := fmt.Sprintf("%s://%s/%s", u.Scheme, u.Host, sub) return &adoGitUrl{ loginUrl: loginUrl, subscription: sub, projectName: project, - repoName: repoName, }, nil } diff --git a/pkg/git/provider_ado_test.go b/pkg/git/provider_ado_test.go index 63cd089b..93828cc2 100644 --- a/pkg/git/provider_ado_test.go +++ b/pkg/git/provider_ado_test.go @@ -114,11 +114,9 @@ func Test_parseAdoUrl(t *testing.T) { loginUrl: "https://dev.azure.com/SUB", subscription: "SUB", projectName: "PROJECT", - repoName: "REPO", - }, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return false - }}, + }, wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return false + }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 6828afb6cabc0231a43cb79e39b90e049ab4705f Mon Sep 17 00:00:00 2001 From: rumstead Date: Wed, 22 Dec 2021 11:41:22 -0500 Subject: [PATCH 8/8] updating a bad comment --- pkg/git/provider_ado.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/git/provider_ado.go b/pkg/git/provider_ado.go index 1211d03a..074f06cd 100644 --- a/pkg/git/provider_ado.go +++ b/pkg/git/provider_ado.go @@ -82,9 +82,7 @@ func (a *adoGitUrl) GetProjectName() string { return a.projectName } -// getLoginUrl parses a URL and returns the URL with only first part of the path -// this path should be the subscription in Azure DevOps -// ie, https://dev.azure.com/SUB/PROJECT/_git/REPO would return https://dev.azure.com/SUB +// getLoginUrl parses a URL to retrieve the subscription and project name func parseAdoUrl(host string) (*adoGitUrl, error) { u, err := url.Parse(host) if err != nil {