diff --git a/.github/workflows/e2e-bootstrap.yaml b/.github/workflows/e2e-bootstrap.yaml index 7de6b00c77..39c41f0c0f 100644 --- a/.github/workflows/e2e-bootstrap.yaml +++ b/.github/workflows/e2e-bootstrap.yaml @@ -53,16 +53,22 @@ jobs: run: | /tmp/flux bootstrap github --manifests ./manifests/install/ \ --owner=fluxcd-testing \ + --image-pull-secret=ghcr-auth \ + --registry-creds=fluxcd:$GITHUB_TOKEN \ --repository=${{ steps.vars.outputs.test_repo_name }} \ --branch=main \ --path=test-cluster \ --team=team-z env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} + - name: verify image pull secret + run: | + kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson - name: bootstrap no-op run: | /tmp/flux bootstrap github --manifests ./manifests/install/ \ --owner=fluxcd-testing \ + --image-pull-secret=ghcr-auth \ --repository=${{ steps.vars.outputs.test_repo_name }} \ --branch=main \ --path=test-cluster \ diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go index 7f0024f273..5f375fee79 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -52,8 +52,9 @@ type bootstrapFlags struct { extraComponents []string requiredComponents []string - registry string - imagePullSecret string + registry string + registryCredential string + imagePullSecret string secretName string tokenAuth bool @@ -98,6 +99,8 @@ func init() { bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd", "container registry where the Flux controller images are published") + bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registryCredential, "registry-creds", "", + "container registry credentials in the format 'user:password', requires --image-pull-secret to be set") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.imagePullSecret, "image-pull-secret", "", "Kubernetes secret name used for pulling the controller images from a private registry") @@ -181,6 +184,14 @@ func bootstrapValidate() error { return err } + if bootstrapArgs.registryCredential != "" && bootstrapArgs.imagePullSecret == "" { + return fmt.Errorf("--registry-creds requires --image-pull-secret to be set") + } + + if bootstrapArgs.registryCredential != "" && len(strings.Split(bootstrapArgs.registryCredential, ":")) != 2 { + return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") + } + return nil } diff --git a/cmd/flux/bootstrap_bitbucket_server.go b/cmd/flux/bootstrap_bitbucket_server.go index f4caf62da5..c26515f62c 100644 --- a/cmd/flux/bootstrap_bitbucket_server.go +++ b/cmd/flux/bootstrap_bitbucket_server.go @@ -196,6 +196,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, diff --git a/cmd/flux/bootstrap_git.go b/cmd/flux/bootstrap_git.go index eb66b0ad73..7610604014 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -28,6 +28,9 @@ import ( "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" + "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/bootstrap" @@ -35,8 +38,6 @@ import ( "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" - "github.com/fluxcd/pkg/git" - "github.com/fluxcd/pkg/git/gogit" ) var bootstrapGitCmd = &cobra.Command{ @@ -201,6 +202,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, diff --git a/cmd/flux/bootstrap_gitea.go b/cmd/flux/bootstrap_gitea.go index 37d53c9266..48b18b0ed7 100644 --- a/cmd/flux/bootstrap_gitea.go +++ b/cmd/flux/bootstrap_gitea.go @@ -184,6 +184,7 @@ func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, diff --git a/cmd/flux/bootstrap_github.go b/cmd/flux/bootstrap_github.go index c2860d0557..a82fb1ce4a 100644 --- a/cmd/flux/bootstrap_github.go +++ b/cmd/flux/bootstrap_github.go @@ -191,6 +191,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, diff --git a/cmd/flux/bootstrap_gitlab.go b/cmd/flux/bootstrap_gitlab.go index f281a8838d..15716623dc 100644 --- a/cmd/flux/bootstrap_gitlab.go +++ b/cmd/flux/bootstrap_gitlab.go @@ -216,6 +216,7 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, diff --git a/cmd/flux/install.go b/cmd/flux/install.go index 9266526500..390f8616c4 100644 --- a/cmd/flux/install.go +++ b/cmd/flux/install.go @@ -21,16 +21,20 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/manifoldco/promptui" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/yaml" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/status" ) @@ -66,6 +70,7 @@ type installFlags struct { defaultComponents []string extraComponents []string registry string + registryCredential string imagePullSecret string branch string watchAllNamespaces bool @@ -92,6 +97,8 @@ func init() { installCmd.Flags().StringVar(&installArgs.manifestsPath, "manifests", "", "path to the manifest directory") installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry, "container registry where the toolkit images are published") + installCmd.Flags().StringVar(&installArgs.registryCredential, "registry-creds", "", + "container registry credentials in the format 'user:password', requires --image-pull-secret to be set") installCmd.Flags().StringVar(&installArgs.imagePullSecret, "image-pull-secret", "", "Kubernetes secret name used for pulling the toolkit images from a private registry") installCmd.Flags().BoolVar(&installArgs.watchAllNamespaces, "watch-all-namespaces", rootArgs.defaults.WatchAllNamespaces, @@ -124,6 +131,14 @@ func installCmdRun(cmd *cobra.Command, args []string) error { return err } + if installArgs.registryCredential != "" && installArgs.imagePullSecret == "" { + return fmt.Errorf("--registry-creds requires --image-pull-secret to be set") + } + + if installArgs.registryCredential != "" && len(strings.Split(installArgs.registryCredential, ":")) != 2 { + return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") + } + if ver, err := getVersion(installArgs.version); err != nil { return err } else { @@ -154,6 +169,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: components, Registry: installArgs.registry, + RegistryCredential: installArgs.registryCredential, ImagePullSecret: installArgs.imagePullSecret, WatchAllNamespaces: installArgs.watchAllNamespaces, NetworkPolicy: installArgs.networkPolicy, @@ -224,6 +240,29 @@ func installCmdRun(cmd *cobra.Command, args []string) error { fmt.Fprintln(os.Stderr, applyOutput) + if opts.ImagePullSecret != "" && opts.RegistryCredential != "" { + logger.Actionf("generating image pull secret %s", opts.ImagePullSecret) + credentials := strings.SplitN(opts.RegistryCredential, ":", 2) + secretOpts := sourcesecret.Options{ + Name: opts.ImagePullSecret, + Namespace: opts.Namespace, + Registry: opts.Registry, + Username: credentials[0], + Password: credentials[1], + } + imagePullSecret, err := sourcesecret.Generate(secretOpts) + if err != nil { + return fmt.Errorf("install failed: %w", err) + } + var s corev1.Secret + if err := yaml.Unmarshal([]byte(imagePullSecret.Content), &s); err != nil { + return fmt.Errorf("install failed: %w", err) + } + if err := upsertSecret(ctx, kubeClient, s); err != nil { + return fmt.Errorf("install failed: %w", err) + } + } + kubeConfig, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return fmt.Errorf("install failed: %w", err) diff --git a/cmd/flux/install_test.go b/cmd/flux/install_test.go index 5cb0fa6643..161a3d36fb 100644 --- a/cmd/flux/install_test.go +++ b/cmd/flux/install_test.go @@ -42,6 +42,11 @@ func TestInstall(t *testing.T) { args: "install unexpectedPosArg --namespace=example", assert: assertError(`unknown command "unexpectedPosArg" for "flux install"`), }, + { + name: "missing image pull secret", + args: "install --registry-creds=fluxcd:test", + assert: assertError(`--registry-creds requires --image-pull-secret to be set`), + }, } for _, tt := range tests { diff --git a/pkg/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go index 10fda94149..bca036275e 100644 --- a/pkg/bootstrap/bootstrap.go +++ b/pkg/bootstrap/bootstrap.go @@ -173,6 +173,26 @@ func reconcileSecret(ctx context.Context, kube client.Client, secret corev1.Secr return kube.Update(ctx, &existing) } +func reconcileImagePullSecret(ctx context.Context, kube client.Client, installOpts install.Options) error { + credentials := strings.SplitN(installOpts.RegistryCredential, ":", 2) + dcj, err := sourcesecret.GenerateDockerConfigJson(installOpts.Registry, credentials[0], credentials[1]) + if err != nil { + return fmt.Errorf("failed to generate docker config json: %w", err) + } + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: installOpts.Namespace, + Name: installOpts.ImagePullSecret, + }, + StringData: map[string]string{ + corev1.DockerConfigJsonKey: string(dcj), + }, + Type: corev1.SecretTypeDockerConfigJson, + } + return reconcileSecret(ctx, kube, secret) +} + func kustomizationPathDiffers(ctx context.Context, kube client.Client, objKey client.ObjectKey, path string) (string, error) { var k kustomizev1.Kustomization if err := kube.Get(ctx, objKey, &k); err != nil { diff --git a/pkg/bootstrap/bootstrap_plain_git.go b/pkg/bootstrap/bootstrap_plain_git.go index ceb503bc93..78af0c8851 100644 --- a/pkg/bootstrap/bootstrap_plain_git.go +++ b/pkg/bootstrap/bootstrap_plain_git.go @@ -207,6 +207,14 @@ func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifest b.logger.Successf("installed components") } + // Reconcile image pull secret if needed + if options.ImagePullSecret != "" && options.RegistryCredential != "" { + if err := reconcileImagePullSecret(ctx, b.kube, options); err != nil { + return fmt.Errorf("failed to reconcile image pull secret: %w", err) + } + b.logger.Successf("reconciled image pull secret %s", options.ImagePullSecret) + } + b.logger.Successf("reconciled components") return nil } diff --git a/pkg/manifestgen/install/options.go b/pkg/manifestgen/install/options.go index a456007b8b..6b848eae29 100644 --- a/pkg/manifestgen/install/options.go +++ b/pkg/manifestgen/install/options.go @@ -26,6 +26,7 @@ type Options struct { ComponentsExtra []string EventsAddr string Registry string + RegistryCredential string ImagePullSecret string WatchAllNamespaces bool NetworkPolicy bool @@ -46,6 +47,7 @@ func MakeDefaultOptions() Options { ComponentsExtra: []string{"image-reflector-controller", "image-automation-controller"}, EventsAddr: "", Registry: "ghcr.io/fluxcd", + RegistryCredential: "", ImagePullSecret: "", WatchAllNamespaces: true, NetworkPolicy: true, diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index f468fa726d..6314eefdea 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -83,7 +83,7 @@ func Generate(options Options) (*manifestgen.Manifest, error) { var dockerCfgJson []byte if options.Registry != "" { - dockerCfgJson, err = generateDockerConfigJson(options.Registry, options.Username, options.Password) + dockerCfgJson, err = GenerateDockerConfigJson(options.Registry, options.Username, options.Password) if err != nil { return nil, fmt.Errorf("failed to generate json for docker config: %w", err) } @@ -223,7 +223,7 @@ func resourceToString(data []byte) string { return string(data) } -func generateDockerConfigJson(url, username, password string) ([]byte, error) { +func GenerateDockerConfigJson(url, username, password string) ([]byte, error) { cred := fmt.Sprintf("%s:%s", username, password) auth := base64.StdEncoding.EncodeToString([]byte(cred)) cfg := DockerConfigJSON{