From 46f9fc194c01991d3da319759d9b16c23e18e957 Mon Sep 17 00:00:00 2001
From: Soule BA <soule@weave.works>
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=<project> --username=<user> --repository=<repository name> --hostname=<domain> --token-auth

There is a parity in the capabilities with the other providers.

Signed-off-by: Soule BA <soule@weave.works>
---
 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=<my-token>
+
+  # Run bootstrap for a private repository using HTTPS token authentication
+  flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --hostname=<domain> --token-auth
+
+  # Run bootstrap for a private repository using SSH authentication
+  flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --hostname=<domain>
+
+  # Run bootstrap for a repository path
+  flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --path=dev-cluster --hostname=<domain>
+
+  # Run bootstrap for a public repository on a personal account
+  flux bootstrap bitbucket-server --owner=<user> --repository=<repository name> --private=false --personal --hostname=<domain> --token-auth
+
+  # Run bootstrap for a an existing repository with a branch named main
+  flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --branch=main --hostname=<domain> --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
-- 
GitLab