From 46f9fc194c01991d3da319759d9b16c23e18e957 Mon Sep 17 00:00:00 2001 From: Soule BA Date: Wed, 27 Oct 2021 15:37:55 +0200 Subject: [PATCH] Add stash provider bootstrap support The new command set is: flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= --token-auth There is a parity in the capabilities with the other providers. Signed-off-by: Soule BA --- cmd/flux/bootstrap_bitbucket_server.go | 268 +++++++++++++++++++++++ go.mod | 2 +- go.sum | 6 +- internal/bootstrap/bootstrap_provider.go | 116 +++++----- internal/bootstrap/provider/factory.go | 9 + internal/bootstrap/provider/provider.go | 5 + 6 files changed, 351 insertions(+), 55 deletions(-) create mode 100644 cmd/flux/bootstrap_bitbucket_server.go diff --git a/cmd/flux/bootstrap_bitbucket_server.go b/cmd/flux/bootstrap_bitbucket_server.go new file mode 100644 index 00000000..20a6b3f3 --- /dev/null +++ b/cmd/flux/bootstrap_bitbucket_server.go @@ -0,0 +1,268 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/bootstrap" + "github.com/fluxcd/flux2/internal/bootstrap/git/gogit" + "github.com/fluxcd/flux2/internal/bootstrap/provider" + "github.com/fluxcd/flux2/internal/flags" + "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/pkg/manifestgen/install" + "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/pkg/manifestgen/sync" +) + +var bootstrapBServerCmd = &cobra.Command{ + Use: "bitbucket-server", + Short: "Bootstrap toolkit components in a Bitbucket Server repository", + Long: `The bootstrap bitbucket-server command creates the Bitbucket Server repository if it doesn't exists and +commits the toolkit components manifests to the master branch. +Then it configures the target cluster to synchronize with the repository. +If the toolkit components are present on the cluster, +the bootstrap command will perform an upgrade if needed.`, + Example: ` # Create a Bitbucket Server API token and export it as an env var + export BITBUCKET_TOKEN= + + # Run bootstrap for a private repository using HTTPS token authentication + flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= --token-auth + + # Run bootstrap for a private repository using SSH authentication + flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= + + # Run bootstrap for a repository path + flux bootstrap bitbucket-server --owner= --username= --repository= --path=dev-cluster --hostname= + + # Run bootstrap for a public repository on a personal account + flux bootstrap bitbucket-server --owner= --repository= --private=false --personal --hostname= --token-auth + + # Run bootstrap for a an existing repository with a branch named main + flux bootstrap bitbucket-server --owner= --username= --repository= --branch=main --hostname= --token-auth`, + RunE: bootstrapBServerCmdRun, +} + +const ( + bServerDefaultPermission = "push" + bServerTokenEnvVar = "BITBUCKET_TOKEN" +) + +type bServerFlags struct { + owner string + repository string + interval time.Duration + personal bool + username string + private bool + hostname string + path flags.SafeRelativePath + teams []string + readWriteKey bool + reconcile bool +} + +var bServerArgs bServerFlags + +func init() { + bootstrapBServerCmd.Flags().StringVar(&bServerArgs.owner, "owner", "", "Bitbucket Server user or project name") + bootstrapBServerCmd.Flags().StringVar(&bServerArgs.repository, "repository", "", "Bitbucket Server repository name") + bootstrapBServerCmd.Flags().StringSliceVar(&bServerArgs.teams, "group", []string{}, "Bitbucket Server groups to be given write access (also accepts comma-separated values)") + bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.personal, "personal", false, "if true, the owner is assumed to be a Bitbucket Server user; otherwise a group") + bootstrapBServerCmd.Flags().StringVarP(&bServerArgs.username, "username", "u", "git", "authentication username") + bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.private, "private", true, "if true, the repository is setup or configured as private") + bootstrapBServerCmd.Flags().DurationVar(&bServerArgs.interval, "interval", time.Minute, "sync interval") + bootstrapBServerCmd.Flags().StringVar(&bServerArgs.hostname, "hostname", "", "Bitbucket Server hostname") + bootstrapBServerCmd.Flags().Var(&bServerArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") + bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") + bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") + + bootstrapCmd.AddCommand(bootstrapBServerCmd) +} + +func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { + bitbucketToken := os.Getenv(bServerTokenEnvVar) + if bitbucketToken == "" { + var err error + bitbucketToken, err = readPasswordFromStdin("Please enter your Bitbucket personal access token (PAT): ") + if err != nil { + return fmt.Errorf("could not read token: %w", err) + } + } + + if bServerArgs.hostname == "" { + return fmt.Errorf("invalid hostname %q", bServerArgs.hostname) + } + + if err := bootstrapValidate(); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + if err != nil { + return err + } + + // Manifest base + if ver, err := getVersion(bootstrapArgs.version); err == nil { + bootstrapArgs.version = ver + } + manifestsBase, err := buildEmbeddedManifestBase() + if err != nil { + return err + } + defer os.RemoveAll(manifestsBase) + + user := bServerArgs.username + if bServerArgs.personal { + user = bServerArgs.owner + } + + // Build Bitbucket Server provider + providerCfg := provider.Config{ + Provider: provider.GitProviderStash, + Hostname: bServerArgs.hostname, + Username: user, + Token: bitbucketToken, + } + + providerClient, err := provider.BuildGitProvider(providerCfg) + if err != nil { + return err + } + + // Lazy go-git repository + tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") + if err != nil { + return fmt.Errorf("failed to create temporary working dir: %w", err) + } + defer os.RemoveAll(tmpDir) + gitClient := gogit.New(tmpDir, &http.BasicAuth{ + Username: user, + Password: bitbucketToken, + }) + + // Install manifest config + installOptions := install.Options{ + BaseURL: rootArgs.defaults.BaseURL, + Version: bootstrapArgs.version, + Namespace: rootArgs.namespace, + Components: bootstrapComponents(), + Registry: bootstrapArgs.registry, + ImagePullSecret: bootstrapArgs.imagePullSecret, + WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, + NetworkPolicy: bootstrapArgs.networkPolicy, + LogLevel: bootstrapArgs.logLevel.String(), + NotificationController: rootArgs.defaults.NotificationController, + ManifestFile: rootArgs.defaults.ManifestFile, + Timeout: rootArgs.timeout, + TargetPath: bServerArgs.path.ToSlash(), + ClusterDomain: bootstrapArgs.clusterDomain, + TolerationKeys: bootstrapArgs.tolerationKeys, + } + if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { + installOptions.BaseURL = customBaseURL + } + + // Source generation and secret config + secretOpts := sourcesecret.Options{ + Name: bootstrapArgs.secretName, + Namespace: rootArgs.namespace, + TargetPath: bServerArgs.path.String(), + ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, + } + if bootstrapArgs.tokenAuth { + if bServerArgs.personal { + secretOpts.Username = bServerArgs.owner + } else { + secretOpts.Username = bServerArgs.username + } + secretOpts.Password = bitbucketToken + + if bootstrapArgs.caFile != "" { + secretOpts.CAFilePath = bootstrapArgs.caFile + } + } else { + secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) + secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) + secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve + secretOpts.SSHHostname = bServerArgs.hostname + + if bootstrapArgs.privateKeyFile != "" { + secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile + } + if bootstrapArgs.sshHostname != "" { + secretOpts.SSHHostname = bootstrapArgs.sshHostname + } + } + + // Sync manifest config + syncOpts := sync.Options{ + Interval: bServerArgs.interval, + Name: rootArgs.namespace, + Namespace: rootArgs.namespace, + Branch: bootstrapArgs.branch, + Secret: bootstrapArgs.secretName, + TargetPath: bServerArgs.path.ToSlash(), + ManifestFile: sync.MakeDefaultOptions().ManifestFile, + GitImplementation: sourceGitArgs.gitImplementation.String(), + RecurseSubmodules: bootstrapArgs.recurseSubmodules, + } + + // Bootstrap config + bootstrapOpts := []bootstrap.GitProviderOption{ + bootstrap.WithProviderRepository(bServerArgs.owner, bServerArgs.repository, bServerArgs.personal), + bootstrap.WithBranch(bootstrapArgs.branch), + bootstrap.WithBootstrapTransportType("https"), + bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), + bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), + bootstrap.WithProviderTeamPermissions(mapTeamSlice(bServerArgs.teams, bServerDefaultPermission)), + bootstrap.WithReadWriteKeyPermissions(bServerArgs.readWriteKey), + bootstrap.WithKubeconfig(rootArgs.kubeconfig, rootArgs.kubecontext), + bootstrap.WithLogger(logger), + } + if bootstrapArgs.sshHostname != "" { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) + } + if bootstrapArgs.tokenAuth { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) + } + if !bServerArgs.private { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) + } + if bServerArgs.reconcile { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) + } + + // Setup bootstrapper with constructed configs + b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) + if err != nil { + return err + } + + // Run + return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) +} diff --git a/go.mod b/go.mod index 9d39b8bb..0109dbbc 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Masterminds/semver/v3 v3.1.0 github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 github.com/cyphar/filepath-securejoin v0.2.2 - github.com/fluxcd/go-git-providers v0.3.1 + github.com/fluxcd/go-git-providers v0.3.2 github.com/fluxcd/helm-controller/api v0.13.0 github.com/fluxcd/image-automation-controller/api v0.17.1 github.com/fluxcd/image-reflector-controller/api v0.13.2 diff --git a/go.sum b/go.sum index b3235ae2..8ccdace1 100644 --- a/go.sum +++ b/go.sum @@ -223,8 +223,8 @@ github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZM github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/go-git-providers v0.3.1 h1:9B3b7mK3XmMxZzcbes3xEJTnQlhkNURhmOY1kLijnZA= -github.com/fluxcd/go-git-providers v0.3.1/go.mod h1:enIPrXnSOBxahS6rngohpG3d/QZ3yjjy/w+agbp97ZI= +github.com/fluxcd/go-git-providers v0.3.2 h1:89dzg5SCAwdNsLjD4GvCVWo9zNKUDkea6shjBJEfspg= +github.com/fluxcd/go-git-providers v0.3.2/go.mod h1:enIPrXnSOBxahS6rngohpG3d/QZ3yjjy/w+agbp97ZI= github.com/fluxcd/helm-controller/api v0.13.0 h1:f9SwsHjqbWfeHMEtpr9wfdbMm0HQ2dL8bVayp2QyPxs= github.com/fluxcd/helm-controller/api v0.13.0/go.mod h1:zWmzV0s2SU4rEIGLPTt+dsaMs40OsNQgSgOATgJmxB0= github.com/fluxcd/image-automation-controller/api v0.17.1 h1:nINAsH6ERKItuWQSH2/Iovjn6a/fu/n7WRFVrloryFE= @@ -472,6 +472,7 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -480,6 +481,7 @@ github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= diff --git a/internal/bootstrap/bootstrap_provider.go b/internal/bootstrap/bootstrap_provider.go index 52bc7aed..8f475b99 100644 --- a/internal/bootstrap/bootstrap_provider.go +++ b/internal/bootstrap/bootstrap_provider.go @@ -30,6 +30,7 @@ import ( "github.com/fluxcd/go-git-providers/gitprovider" "github.com/fluxcd/flux2/internal/bootstrap/git" + "github.com/fluxcd/flux2/internal/bootstrap/provider" "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/pkg/manifestgen/sync" ) @@ -37,9 +38,11 @@ import ( type GitProviderBootstrapper struct { *PlainGitBootstrapper - owner string - repository string - personal bool + owner string + repositoryName string + repository gitprovider.UserRepository + + personal bool description string defaultBranch string @@ -80,23 +83,23 @@ type GitProviderOption interface { applyGitProvider(b *GitProviderBootstrapper) } -func WithProviderRepository(owner, repository string, personal bool) GitProviderOption { +func WithProviderRepository(owner, repositoryName string, personal bool) GitProviderOption { return providerRepositoryOption{ - owner: owner, - repository: repository, - personal: personal, + owner: owner, + repositoryName: repositoryName, + personal: personal, } } type providerRepositoryOption struct { - owner string - repository string - personal bool + owner string + repositoryName string + personal bool } func (o providerRepositoryOption) applyGitProvider(b *GitProviderBootstrapper) { b.owner = o.owner - b.repository = o.repository + b.repositoryName = o.repositoryName b.personal = o.personal } @@ -181,19 +184,19 @@ func (o reconcileOption) applyGitProvider(b *GitProviderBootstrapper) { } func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error { - repo, err := b.getRepository(ctx) - if err != nil { - return err + if b.repository == nil { + return errors.New("repository is required") } + if b.url == "" { - bootstrapURL, err := b.getCloneURL(repo, gitprovider.TransportType(b.bootstrapTransportType)) + bootstrapURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.bootstrapTransportType)) if err != nil { return err } WithRepositoryURL(bootstrapURL).applyGit(b.PlainGitBootstrapper) } if options.URL == "" { - syncURL, err := b.getCloneURL(repo, gitprovider.TransportType(b.syncTransportType)) + syncURL, err := b.getCloneURL(b.repository, gitprovider.TransportType(b.syncTransportType)) if err != nil { return err } @@ -211,7 +214,6 @@ func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, optio func (b *GitProviderBootstrapper) ReconcileRepository(ctx context.Context) error { var repo gitprovider.UserRepository var err error - if b.personal { repo, err = b.reconcileUserRepository(ctx) } else { @@ -221,36 +223,37 @@ func (b *GitProviderBootstrapper) ReconcileRepository(ctx context.Context) error return err } - cloneURL := repo.Repository().GetCloneURL(gitprovider.TransportType(b.bootstrapTransportType)) - // TODO(hidde): https://github.com/fluxcd/go-git-providers/issues/55 - if strings.HasPrefix(cloneURL, "https://https://") { - cloneURL = strings.TrimPrefix(cloneURL, "https://") + cloneURL, err := b.getCloneURL(repo, gitprovider.TransportType(b.bootstrapTransportType)) + if err != nil { + return err } + + b.repository = repo WithRepositoryURL(cloneURL).applyGit(b.PlainGitBootstrapper) return err } func (b *GitProviderBootstrapper) reconcileDeployKey(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error { + if b.repository == nil { + return errors.New("repository is required") + } + ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey] if !ok { return nil } b.logger.Successf("public key: %s", strings.TrimSpace(ppk)) - repo, err := b.getRepository(ctx) - if err != nil { - return err - } - name := deployKeyName(options.Namespace, b.branch, options.Name, options.TargetPath) deployKeyInfo := newDeployKeyInfo(name, ppk, b.readWriteKey) - var changed bool - if _, changed, err = repo.DeployKeys().Reconcile(ctx, deployKeyInfo); err != nil { + + _, changed, err := b.repository.DeployKeys().Reconcile(ctx, deployKeyInfo) + if err != nil { return err } if changed { - b.logger.Successf("configured deploy key %q for %q", deployKeyInfo.Name, repo.Repository().String()) + b.logger.Successf("configured deploy key %q for %q", deployKeyInfo.Name, b.repository.Repository().String()) } return nil } @@ -267,9 +270,12 @@ func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (g // Construct the repository and other configuration objects // go-git-provider likes to work with - subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repository) - orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs) - repoRef := newOrgRepositoryRef(orgRef, repoName) + subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName) + orgRef, err := b.getOrganization(ctx, subOrgs) + if err != nil { + return nil, fmt.Errorf("failed to create new Git repository for the organization %q: %w", orgRef.String(), err) + } + repoRef := newOrgRepositoryRef(*orgRef, repoName) repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility) // Reconcile the organization repository @@ -343,7 +349,7 @@ func (b *GitProviderBootstrapper) reconcileUserRepository(ctx context.Context) ( // Construct the repository and other metadata objects // go-git-provider likes to work with. - _, repoName := splitSubOrganizationsFromRepositoryName(b.repository) + _, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName) userRef := newUserRef(b.provider.SupportedDomain(), b.owner) repoRef := newUserRepositoryRef(userRef, repoName) repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility) @@ -383,21 +389,22 @@ func (b *GitProviderBootstrapper) reconcileUserRepository(ctx context.Context) ( return repo, nil } -// getRepository retrieves and returns the gitprovider.UserRepository -// for organization and user repositories using the -// GitProviderBootstrapper values. -// As gitprovider.OrgRepository is a superset of gitprovider.UserRepository, this -// type is returned. -func (b *GitProviderBootstrapper) getRepository(ctx context.Context) (gitprovider.UserRepository, error) { - subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repository) +// getOrganization retrieves and returns the gitprovider.Organization +// using the GitProviderBootstrapper values. +func (b *GitProviderBootstrapper) getOrganization(ctx context.Context, subOrgs []string) (*gitprovider.OrganizationRef, error) { + orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs) + // With Stash get the organization to be sure to get the correct key + if string(b.provider.ProviderID()) == string(provider.GitProviderStash) { + org, err := b.provider.Organizations().Get(ctx, orgRef) + if err != nil { + return nil, fmt.Errorf("failed to get Git organization: %w", err) + } - if b.personal { - userRef := newUserRef(b.provider.SupportedDomain(), b.owner) - return b.provider.UserRepositories().Get(ctx, newUserRepositoryRef(userRef, repoName)) - } + orgRef = org.Organization() - orgRef := newOrganizationRef(b.provider.SupportedDomain(), b.owner, subOrgs) - return b.provider.OrgRepositories().Get(ctx, newOrgRepositoryRef(orgRef, repoName)) + return &orgRef, nil + } + return &orgRef, nil } // getCloneURL returns the Git clone URL for the given @@ -405,18 +412,23 @@ func (b *GitProviderBootstrapper) getRepository(ctx context.Context) (gitprovide // gitprovider.TransportTypeSSH and a custom SSH hostname is configured, // the hostname of the URL will be modified to this hostname. func (b *GitProviderBootstrapper) getCloneURL(repository gitprovider.UserRepository, transport gitprovider.TransportType) (string, error) { - u := repository.Repository().GetCloneURL(transport) + var url string + if cloner, ok := repository.(gitprovider.CloneableURL); ok { + return cloner.GetCloneURL("", transport), nil + } + + url = repository.Repository().GetCloneURL(transport) // TODO(hidde): https://github.com/fluxcd/go-git-providers/issues/55 - if strings.HasPrefix(u, "https://https://") { - u = strings.TrimPrefix(u, "https://") + if strings.HasPrefix(url, "https://https://") { + url = strings.TrimPrefix(url, "https://") } var err error if transport == gitprovider.TransportTypeSSH && b.sshHostname != "" { - if u, err = setHostname(u, b.sshHostname); err != nil { - err = fmt.Errorf("failed to set SSH hostname for URL %q: %w", u, err) + if url, err = setHostname(url, b.sshHostname); err != nil { + err = fmt.Errorf("failed to set SSH hostname for URL %q: %w", url, err) } } - return u, err + return url, err } // splitSubOrganizationsFromRepositoryName removes any prefixed sub diff --git a/internal/bootstrap/provider/factory.go b/internal/bootstrap/provider/factory.go index 29f47d98..1790963a 100644 --- a/internal/bootstrap/provider/factory.go +++ b/internal/bootstrap/provider/factory.go @@ -22,6 +22,7 @@ import ( "github.com/fluxcd/go-git-providers/github" "github.com/fluxcd/go-git-providers/gitlab" "github.com/fluxcd/go-git-providers/gitprovider" + "github.com/fluxcd/go-git-providers/stash" ) // BuildGitProvider builds a gitprovider.Client for the provided @@ -51,6 +52,14 @@ func BuildGitProvider(config Config) (gitprovider.Client, error) { if client, err = gitlab.NewClient(config.Token, "", opts...); err != nil { return nil, err } + case GitProviderStash: + opts := []gitprovider.ClientOption{} + if config.Hostname != "" { + opts = append(opts, gitprovider.WithDomain(config.Hostname)) + } + if client, err = stash.NewStashClient(config.Username, config.Token, opts...); err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported Git provider '%s'", config.Provider) } diff --git a/internal/bootstrap/provider/provider.go b/internal/bootstrap/provider/provider.go index 1755e029..face6cc1 100644 --- a/internal/bootstrap/provider/provider.go +++ b/internal/bootstrap/provider/provider.go @@ -22,6 +22,7 @@ type GitProvider string const ( GitProviderGitHub GitProvider = "github" GitProviderGitLab GitProvider = "gitlab" + GitProviderStash GitProvider = "stash" ) // Config defines the configuration for connecting to a GitProvider. @@ -33,6 +34,10 @@ type Config struct { // e.g. github.example.com. Hostname string + // Username contains the username used to authenticate with + // the Provider. + Username string + // Token contains the token used to authenticate with the // Provider. Token string