diff --git a/cmd/install/install.go b/cmd/install/install.go index a38def732fd..e022f1ecb7b 100644 --- a/cmd/install/install.go +++ b/cmd/install/install.go @@ -155,6 +155,7 @@ type Options struct { ScaleFromZeroCreds string ScaleFromZeroCredentialsSecret string ScaleFromZeroCredentialsSecretKey string + RenderSensitive bool } func (o *Options) Validate() error { diff --git a/cmd/install/install_render.go b/cmd/install/install_render.go index c18eba8aa26..e420ecaed8d 100644 --- a/cmd/install/install_render.go +++ b/cmd/install/install_render.go @@ -9,6 +9,7 @@ import ( hyperapi "github.com/openshift/hypershift/support/api" "github.com/openshift/hypershift/support/config" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" @@ -69,6 +70,7 @@ func NewRenderCommand(opts *Options) *cobra.Command { cmd.Flags().StringVar(&opts.Format, "format", RenderFormatYaml, fmt.Sprintf("Output format for the manifests, supports %s and %s", RenderFormatYaml, RenderFormatJson)) cmd.Flags().StringVar(&opts.OutputTypes, "outputs", string(OutputAll), fmt.Sprintf("Which manifests to output, one of %s, %s, or %s. Output CRDs separately to allow applying them first and waiting for them to be established.", OutputAll, OutputCRDs, OutputResources)) cmd.Flags().StringVar(&opts.OutputFile, "output-file", "", "File to write the rendered manifests to. Writes to STDOUT if not specified.") + cmd.Flags().BoolVar(&opts.RenderSensitive, "render-sensitive", false, "Render secrets in the output. By default secrets are excluded to avoid leaking private key material into GitOps repositories") cmd.MarkFlagsMutuallyExclusive("template", "outputs") cmd.RunE = func(cmd *cobra.Command, args []string) error { @@ -103,6 +105,10 @@ func RenderHyperShiftOperator(ctx context.Context, cmdOut io.Writer, opts *Optio return err } + if opts.Template && !opts.RenderSensitive { + return fmt.Errorf("--template requires --render-sensitive=true because Template output can embed Secret objects") + } + var crds []crclient.Object var objects []crclient.Object @@ -129,6 +135,18 @@ func RenderHyperShiftOperator(ctx context.Context, cmdOut io.Writer, opts *Optio case OutputResources: objectsToRender = objects } + + if !opts.RenderSensitive { + filtered := make([]crclient.Object, 0, len(objectsToRender)) + for _, obj := range objectsToRender { + if _, isSecret := obj.(*corev1.Secret); isSecret { + continue + } + filtered = append(filtered, obj) + } + objectsToRender = filtered + } + var out io.Writer if opts.OutputFile != "" { file, err := os.Create(opts.OutputFile) diff --git a/cmd/install/install_render_test.go b/cmd/install/install_render_test.go index fe31c1835a1..be2c72fe470 100644 --- a/cmd/install/install_render_test.go +++ b/cmd/install/install_render_test.go @@ -65,7 +65,7 @@ func TestMultiDocYamlRendering(t *testing.T) { } func TestTemplateYamlRendering(t *testing.T) { - template, err := ExecuteTemplateYamlGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "yaml", "--template"}) + template, err := ExecuteTemplateYamlGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "yaml", "--template", "--render-sensitive"}) if err != nil { t.Fatal(err) } @@ -104,6 +104,17 @@ func ExecuteJsonGenerationCommand(args []string) (map[string]interface{}, error) return doc, nil } +func TestWhenTemplateWithoutRenderSensitiveItShouldFail(t *testing.T) { + _, err := ExecuteTestCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "yaml", "--template"}) + if err == nil { + t.Fatal("expected error when using --template without --render-sensitive") + } + expectedMsg := "--template requires --render-sensitive=true because Template output can embed Secret objects" + if err.Error() != expectedMsg { + t.Fatalf("expected error message %q, got %q", expectedMsg, err.Error()) + } +} + func TestJsonListRendering(t *testing.T) { doc, err := ExecuteJsonGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "json"}) if err != nil { @@ -120,7 +131,7 @@ func TestJsonListRendering(t *testing.T) { } func TestJsonTemplateRendering(t *testing.T) { - doc, err := ExecuteJsonGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "json", "--template"}) + doc, err := ExecuteJsonGenerationCommand([]string{"--oidc-storage-provider-s3-bucket-name", "bucket", "--oidc-storage-provider-s3-region", "us-east-1", "--oidc-storage-provider-s3-secret", "secret", "render", "--format", "json", "--template", "--render-sensitive"}) if err != nil { t.Fatal(err) } diff --git a/cmd/install/install_test.go b/cmd/install/install_test.go index f6ea7ca255b..b01725f0b92 100644 --- a/cmd/install/install_test.go +++ b/cmd/install/install_test.go @@ -1,6 +1,7 @@ package install import ( + "bytes" "context" "io" "io/fs" @@ -490,6 +491,64 @@ func TestSetupCRDs(t *testing.T) { } } +func TestRenderHyperShiftOperator_RenderSensitive(t *testing.T) { + tests := []struct { + name string + renderSensitive bool + expectSecrets bool + }{ + { + name: "When render-sensitive is false it should exclude secrets from output", + renderSensitive: false, + expectSecrets: false, + }, + { + name: "When render-sensitive is true it should include secrets in output", + renderSensitive: true, + expectSecrets: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + var buf bytes.Buffer + opts := &Options{ + PrivatePlatform: string(hyperv1.NonePlatform), + EnableDefaultingWebhook: true, + EnableValidatingWebhook: true, + EnableConversionWebhook: true, + RenderSensitive: tc.renderSensitive, + Format: RenderFormatYaml, + OutputTypes: string(OutputAll), + } + err := RenderHyperShiftOperator(t.Context(), &buf, opts) + g.Expect(err).ToNot(HaveOccurred()) + + var secretNames []string + decodedCount := 0 + for doc := range strings.SplitSeq(buf.String(), "\n---\n") { + if strings.TrimSpace(doc) == "" { + continue + } + obj, _, err := hyperapi.YamlSerializer.Decode([]byte(doc), nil, nil) + g.Expect(err).ToNot(HaveOccurred(), "failed to decode rendered manifest") + decodedCount++ + if secret, ok := obj.(*corev1.Secret); ok { + secretNames = append(secretNames, secret.Name) + } + } + g.Expect(decodedCount).To(BeNumerically(">", 0), "expected rendered manifests to be decodable") + if tc.expectSecrets { + g.Expect(secretNames).ToNot(BeEmpty(), "expected secrets in rendered output") + } else { + g.Expect(secretNames).To(BeEmpty(), "expected no secrets in rendered output") + } + }) + } +} + func TestHyperShiftOperatorManifests_SharedIngress(t *testing.T) { tests := []struct { name string