From 8a50f4eb2d37ccf183bddeeabd674f9ab1bfab8d Mon Sep 17 00:00:00 2001 From: Timo Furrer Date: Fri, 3 Mar 2023 22:30:46 +0100 Subject: [PATCH] Implement `--deploy-token-auth` in GitLab bootstrapping This change set implements support for the `--deploy-token-auth` option in the `flux bootstrap gitlab` command. That option will reconcile a GitLab Project Deploy Token to use for the authentication of the GitLab git repository. A GitLab Project Deploy Token can be used the same way as a Personal Access Token which is already supported via `--token-auth`. The difference with the GitLab Project Deploy Token is that the token is managed (created, updated, deleted) by Flux and not provided by the user. This change is transparent to the source-controller. A prerequisite for this change is the `fluxcd/go-git-providers` change here: * https://github.com/fluxcd/go-git-providers/pull/191 See related discussion here: https://github.com/fluxcd/flux2/discussions/3595 GitLab Issue here: https://gitlab.com/gitlab-org/gitlab/-/issues/392605 Signed-off-by: Timo Furrer --- cmd/flux/bootstrap_gitlab.go | 40 ++++++++++++----- pkg/bootstrap/bootstrap_provider.go | 69 +++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/cmd/flux/bootstrap_gitlab.go b/cmd/flux/bootstrap_gitlab.go index e5d27229e7..34d877a018 100644 --- a/cmd/flux/bootstrap_gitlab.go +++ b/cmd/flux/bootstrap_gitlab.go @@ -65,7 +65,11 @@ the bootstrap command will perform an upgrade if needed.`, flux bootstrap gitlab --owner= --repository= --hostname= --token-auth # Run bootstrap for a an existing repository with a branch named main - flux bootstrap gitlab --owner= --repository= --branch=main --token-auth`, + flux bootstrap gitlab --owner= --repository= --branch=main --token-auth + + # Run bootstrap for a private repository using Deploy Token authentication + flux bootstrap gitlab --owner= --repository= --deploy-token-auth + `, RunE: bootstrapGitLabCmdRun, } @@ -77,16 +81,17 @@ const ( ) type gitlabFlags struct { - owner string - repository string - interval time.Duration - personal bool - private bool - hostname string - path flags.SafeRelativePath - teams []string - readWriteKey bool - reconcile bool + owner string + repository string + interval time.Duration + personal bool + private bool + hostname string + path flags.SafeRelativePath + teams []string + readWriteKey bool + reconcile bool + deployTokenAuth bool } var gitlabArgs gitlabFlags @@ -102,6 +107,7 @@ func init() { bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") + bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.deployTokenAuth, "deploy-token-auth", false, "when enabled, a Project Deploy Token is generated and will be used instead of the SSH deploy token") bootstrapCmd.AddCommand(bootstrapGitLabCmd) } @@ -123,6 +129,10 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { return err } + if bootstrapArgs.tokenAuth && gitlabArgs.deployTokenAuth { + return fmt.Errorf("--token-auth and --deploy-token-auth cannot be set both.") + } + if err := bootstrapValidate(); err != nil { return err } @@ -225,6 +235,9 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { secretOpts.Username = "git" secretOpts.Password = glToken secretOpts.CAFile = caBundle + } else if gitlabArgs.deployTokenAuth { + // the actual deploy token will be reconciled later + secretOpts.CAFile = caBundle } else { keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) if err != nil { @@ -274,9 +287,12 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) } - if bootstrapArgs.tokenAuth { + if bootstrapArgs.tokenAuth || gitlabArgs.deployTokenAuth { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) } + if gitlabArgs.deployTokenAuth { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithDeployTokenAuth()) + } if !gitlabArgs.private { bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) } diff --git a/pkg/bootstrap/bootstrap_provider.go b/pkg/bootstrap/bootstrap_provider.go index 6360092628..74aa48ef13 100644 --- a/pkg/bootstrap/bootstrap_provider.go +++ b/pkg/bootstrap/bootstrap_provider.go @@ -59,6 +59,8 @@ type GitProviderBootstrapper struct { sshHostname string + useDeployTokenAuth bool + provider gitprovider.Client } @@ -184,6 +186,16 @@ func (o reconcileOption) applyGitProvider(b *GitProviderBootstrapper) { b.reconcile = true } +func WithDeployTokenAuth() GitProviderOption { + return deployTokenAuthOption(true) +} + +type deployTokenAuthOption bool + +func (o deployTokenAuthOption) applyGitProvider(b *GitProviderBootstrapper) { + b.useDeployTokenAuth = true +} + func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error { if b.repository == nil { return errors.New("repository is required") @@ -208,6 +220,26 @@ func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, optio return b.PlainGitBootstrapper.ReconcileSyncConfig(ctx, options) } +func (b *GitProviderBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error { + if b.repository == nil { + return errors.New("repository is required") + } + + if b.useDeployTokenAuth { + deployTokenInfo, err := b.reconcileDeployToken(ctx, options) + if err != nil { + return err + } + + if deployTokenInfo != nil { + options.Username = deployTokenInfo.Username + options.Password = deployTokenInfo.Token + } + } + + return b.PlainGitBootstrapper.ReconcileSourceSecret(ctx, options) +} + // ReconcileRepository reconciles an organization or user repository with the // GitProviderBootstrapper configuration. On success, the URL in the embedded // PlainGitBootstrapper is set to clone URL for the configured protocol. @@ -261,6 +293,32 @@ func (b *GitProviderBootstrapper) reconcileDeployKey(ctx context.Context, secret return nil } +func (b *GitProviderBootstrapper) reconcileDeployToken(ctx context.Context, options sourcesecret.Options) (*gitprovider.DeployTokenInfo, error) { + dts, err := b.repository.DeployTokens() + if err != nil { + return nil, err + } + + b.logger.Actionf("checking to reconcile deploy token for source secret") + name := deployTokenName(options.Namespace, b.branch, options.Name, options.TargetPath) + deployTokenInfo := gitprovider.DeployTokenInfo{Name: name} + + deployToken, changed, err := dts.Reconcile(ctx, deployTokenInfo) + if err != nil { + return nil, err + } + + if changed { + b.logger.Successf("configured deploy token %q for %q", deployTokenInfo.Name, b.repository.Repository().String()) + deployTokenInfo := deployToken.Get() + return &deployTokenInfo, nil + } + + b.logger.Successf("reconciled deploy token for source secret") + + return nil, nil +} + // reconcileOrgRepository reconciles a gitprovider.OrgRepository // with the GitProviderBootstrapper values, including any // gitprovider.TeamAccessInfo configurations. @@ -554,6 +612,17 @@ func deployKeyName(namespace, secretName, branch, path string) string { return name } +func deployTokenName(namespace, secretName, branch, path string) string { + var elems []string + for _, v := range []string{namespace, secretName, branch, path} { + if v == "" { + continue + } + elems = append(elems, v) + } + return strings.Join(elems, "-") +} + // setHostname is a helper to replace the hostname of the given URL. // TODO(hidde): support for this should be added in go-git-providers. func setHostname(URL, hostname string) (string, error) {