diff --git a/pkg/kubeseal/kubeseal.go b/pkg/kubeseal/kubeseal.go index 2de6ca9451..c3779bc0a1 100644 --- a/pkg/kubeseal/kubeseal.go +++ b/pkg/kubeseal/kubeseal.go @@ -226,7 +226,7 @@ func Seal(clientConfig ClientConfig, outputFormat string, in io.Reader, out io.W return err } - for _, secret := range secrets { + for pos, secret := range secrets { if len(secret.Data) == 0 && len(secret.StringData) == 0 && !allowEmptyData { return fmt.Errorf("secret.data is empty in input Secret, assuming this is an error and aborting. To work with empty data, --allow-empty-data can be used") } @@ -270,7 +270,7 @@ func Seal(clientConfig ClientConfig, outputFormat string, in io.Reader, out io.W if err != nil { return err } - if err = sealedSecretOutput(out, outputFormat, codecs, ssecret); err != nil { + if err = sealedSecretOutput(out, outputFormat, codecs, ssecret, needsSeparator(pos, len(secrets))); err != nil { return err } //return nil @@ -278,6 +278,10 @@ func Seal(clientConfig ClientConfig, outputFormat string, in io.Reader, out io.W return nil } +func needsSeparator(position, length int) bool { + return position > 0 && position < length +} + func ValidateSealedSecret(ctx context.Context, clientConfig ClientConfig, controllerNs, controllerName string, in io.Reader) error { conf, err := clientConfig.ClientConfig() if err != nil { @@ -351,7 +355,7 @@ func ReEncryptSealedSecret(ctx context.Context, clientConfig ClientConfig, contr return err } - for _, secret := range secrets { + for pos, secret := range secrets { content, err := json.Marshal(secret) if err != nil { return err @@ -375,20 +379,26 @@ func ReEncryptSealedSecret(ctx context.Context, clientConfig ClientConfig, contr ssecret.SetCreationTimestamp(metav1.Time{}) ssecret.SetDeletionTimestamp(nil) ssecret.Generation = 0 - if err = sealedSecretOutput(out, outputFormat, codecs, ssecret); err != nil { + if err = sealedSecretOutput(out, outputFormat, codecs, ssecret, needsSeparator(pos, len(secrets))); err != nil { return err } } return nil } -func resourceOutput(out io.Writer, outputFormat string, codecs runtimeserializer.CodecFactory, gv runtime.GroupVersioner, obj runtime.Object) error { +func resourceOutput(out io.Writer, outputFormat string, codecs runtimeserializer.CodecFactory, gv runtime.GroupVersioner, obj runtime.Object, needsSeparator bool) error { var contentType string switch strings.ToLower(outputFormat) { case "json", "": contentType = runtime.ContentTypeJSON + if needsSeparator { + fmt.Fprint(out, "\n") + } case "yaml": contentType = runtime.ContentTypeYAML + if needsSeparator { + fmt.Fprint(out, "---\n") + } default: return fmt.Errorf("unsupported output format: %s", outputFormat) } @@ -402,17 +412,11 @@ func resourceOutput(out io.Writer, outputFormat string, codecs runtimeserializer } _, _ = out.Write(buf) - switch contentType { - case runtime.ContentTypeJSON: - fmt.Fprint(out, "\n") - case runtime.ContentTypeYAML: - fmt.Fprint(out, "---\n") - } return nil } -func sealedSecretOutput(out io.Writer, outputFormat string, codecs runtimeserializer.CodecFactory, ssecret *ssv1alpha1.SealedSecret) error { - return resourceOutput(out, outputFormat, codecs, ssv1alpha1.SchemeGroupVersion, ssecret) +func sealedSecretOutput(out io.Writer, outputFormat string, codecs runtimeserializer.CodecFactory, ssecret *ssv1alpha1.SealedSecret, needsSeparator bool) error { + return resourceOutput(out, outputFormat, codecs, ssv1alpha1.SchemeGroupVersion, ssecret, needsSeparator) } func decodeSealedSecret(codecs runtimeserializer.CodecFactory, b []byte) (*ssv1alpha1.SealedSecret, error) { @@ -468,7 +472,7 @@ func SealMergingInto(clientConfig ClientConfig, outputFormat string, in io.Reade // updated sealed secret file in-place avoiding clobbering the file upon rendering errors. var out bytes.Buffer - if err := sealedSecretOutput(&out, outputFormat, codecs, orig); err != nil { + if err := sealedSecretOutput(&out, outputFormat, codecs, orig, false); err != nil { return err } @@ -619,5 +623,5 @@ func UnsealSealedSecret(w io.Writer, in io.Reader, privKeysFilenames []string, o return err } - return resourceOutput(w, outputFormat, codecs, v1.SchemeGroupVersion, sec) + return resourceOutput(w, outputFormat, codecs, v1.SchemeGroupVersion, sec, false) } diff --git a/pkg/kubeseal/kubeseal_test.go b/pkg/kubeseal/kubeseal_test.go index bfa79abe67..675de1dce1 100644 --- a/pkg/kubeseal/kubeseal_test.go +++ b/pkg/kubeseal/kubeseal_test.go @@ -14,6 +14,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "regexp" goruntime "runtime" "strings" "testing" @@ -187,22 +188,35 @@ func TestSealWithMultiDocSecrets(t *testing.T) { } testCases := []struct { - name string - asYaml bool - inputSeparator string - outputFormat string + name string + asYaml bool + inputSeparator string + outputFormat string + checkTrailingChars func(t *testing.T, outBytes []byte) }{ { name: "multi-doc json", asYaml: false, inputSeparator: "\n", outputFormat: "json", + checkTrailingChars: func(t *testing.T, outBytes []byte) { + endWithTrailingNewLine, _ := regexp.Compile("(\n)\n?$") + if endWithTrailingNewLine.Match(outBytes) { + t.Errorf("output should not end with trailing new line") + } + }, }, { name: "multi-doc yaml", asYaml: true, inputSeparator: "---\n", outputFormat: "yaml", + checkTrailingChars: func(t *testing.T, outBytes []byte) { + endWithDashes, _ := regexp.Compile("---(\n)?$") + if endWithDashes.Match(outBytes) { + t.Errorf("output should not end with dashes") + } + }, }, } @@ -210,7 +224,8 @@ func TestSealWithMultiDocSecrets(t *testing.T) { t.Run(tc.name, func(t *testing.T) { s1 := mkTestSecret(t, "foo", "1", withSecretName("s1"), asYAML(tc.asYaml)) s2 := mkTestSecret(t, "bar", "2", withSecretName("s2"), asYAML(tc.asYaml)) - multiDocYaml := fmt.Sprintf("%s%s%s", s1, tc.inputSeparator, s2) + s3 := mkTestSecret(t, "foobar", "3", withSecretName("s3"), asYAML(tc.asYaml)) + multiDocYaml := fmt.Sprintf("%s%s%s%s%s", s1, tc.inputSeparator, s2, tc.inputSeparator, s3) clientConfig := testClientConfig() outputFormat := tc.outputFormat @@ -220,7 +235,7 @@ func TestSealWithMultiDocSecrets(t *testing.T) { t.Fatalf("Error writing to buffer: %v", err) } - t.Logf("input is: %s", inbuf.String()) + t.Logf("input is:\n%s", inbuf.String()) outbuf := bytes.Buffer{} if err := Seal(clientConfig, outputFormat, &inbuf, &outbuf, scheme.Codecs, key, ssv1alpha1.NamespaceWideScope, false, "", ""); err != nil { @@ -228,7 +243,7 @@ func TestSealWithMultiDocSecrets(t *testing.T) { } outBytes := outbuf.Bytes() - t.Logf("output is %s", outBytes) + t.Logf("output is:\n%s", outBytes) decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(outBytes), 4096) var gotSecrets []*ssv1alpha1.SealedSecret @@ -244,7 +259,7 @@ func TestSealWithMultiDocSecrets(t *testing.T) { gotSecrets = append(gotSecrets, &s) } - if got, want := len(gotSecrets), 2; got != want { + if got, want := len(gotSecrets), 3; got != want { t.Errorf("Wrong element output length: got: %d, want: %d", got, want) } @@ -252,10 +267,11 @@ func TestSealWithMultiDocSecrets(t *testing.T) { if got, want := gotSecret.GetNamespace(), "testns"; got != want { t.Errorf("got: %q, want: %q", got, want) } - if got, want := gotSecret.GetName(), []string{"s1", "s2"}; !slices.Contains(want, got) { + if got, want := gotSecret.GetName(), []string{"s1", "s2", "s3"}; !slices.Contains(want, got) { t.Errorf("got: %q, want: %q", got, want) } } + tc.checkTrailingChars(t, outBytes) }) } } @@ -959,7 +975,7 @@ func TestReadPrivKeySecret(t *testing.T) { } // defer os.RemoveAll(tmp.Name()) - if err := resourceOutput(tmp, outputFormat, scheme.Codecs, v1.SchemeGroupVersion, sec); err != nil { + if err := resourceOutput(tmp, outputFormat, scheme.Codecs, v1.SchemeGroupVersion, sec, false); err != nil { t.Fatal(err) } tmp.Close()