From bdfaf84c684e93d0ddaa0dfa645ad49e9542f439 Mon Sep 17 00:00:00 2001 From: Marko Mikulicic Date: Thu, 12 Sep 2019 12:17:30 +0200 Subject: [PATCH] Implement the --merge-into command --- cmd/kubeseal/main.go | 59 ++++++++++++++++- cmd/kubeseal/main_test.go | 130 +++++++++++++++++++++++++++++++++++++- 2 files changed, 185 insertions(+), 4 deletions(-) diff --git a/cmd/kubeseal/main.go b/cmd/kubeseal/main.go index a329c9ec7..5b60f9f14 100644 --- a/cmd/kubeseal/main.go +++ b/cmd/kubeseal/main.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "crypto/rsa" "encoding/json" "errors" @@ -46,6 +47,7 @@ var ( dumpCert = flag.Bool("fetch-cert", false, "Write certificate to stdout. Useful for later use with --cert") printVersion = flag.Bool("version", false, "Print version information and exit") validateSecret = flag.Bool("validate", false, "Validate that the sealed secret can be decrypted") + mergeInto = flag.String("merge-into", "", "Merge items from secret into an existing sealed secret file, updating the file in-place instead of writing to stdout.") reEncrypt bool // re-encrypt command // VERSION set from Makefile @@ -315,7 +317,56 @@ func sealedSecretOutput(out io.Writer, codecs runtimeserializer.CodecFactory, ss return nil } -func run(w io.Writer, controllerNs, controllerName, certFile string, printVersion, validateSecret, reEncrypt, dumpCert bool) error { +func decodeSealedSecret(codecs runtimeserializer.CodecFactory, b []byte) (*ssv1alpha1.SealedSecret, error) { + var ss ssv1alpha1.SealedSecret + if err := runtime.DecodeInto(codecs.UniversalDecoder(), b, &ss); err != nil { + return nil, err + } + return &ss, nil +} + +func sealMergingInto(in io.Reader, filename string, codecs runtimeserializer.CodecFactory, pubKey *rsa.PublicKey) error { + var buf bytes.Buffer + if err := seal(in, &buf, codecs, pubKey); err != nil { + return err + } + + update, err := decodeSealedSecret(codecs, buf.Bytes()) + if err != nil { + return err + } + + b, err := ioutil.ReadFile(filename) + if err != nil { + return err + } + + orig, err := decodeSealedSecret(codecs, b) + if err != nil { + return err + } + + // merge encrypted data and metadata + for k, v := range update.Spec.EncryptedData { + orig.Spec.EncryptedData[k] = v + } + for k, v := range update.Spec.Template.Annotations { + orig.Spec.Template.Annotations[k] = v + } + for k, v := range update.Spec.Template.Labels { + orig.Spec.Template.Labels[k] = v + } + + // updated sealed secret file in-place avoiding clobbering the file upon rendering errors. + var out bytes.Buffer + if err := sealedSecretOutput(&out, codecs, orig); err != nil { + return err + } + + return ioutil.WriteFile(filename, out.Bytes(), 0) +} + +func run(w io.Writer, controllerNs, controllerName, certFile string, printVersion, validateSecret, reEncrypt, dumpCert bool, mergeInto string) error { if printVersion { fmt.Fprintf(w, "kubeseal version: %s\n", VERSION) return nil @@ -345,6 +396,10 @@ func run(w io.Writer, controllerNs, controllerName, certFile string, printVersio return err } + if mergeInto != "" { + return sealMergingInto(os.Stdin, mergeInto, scheme.Codecs, pubKey) + } + return seal(os.Stdin, os.Stdout, scheme.Codecs, pubKey) } @@ -352,7 +407,7 @@ func main() { flag.Parse() goflag.CommandLine.Parse([]string{}) - if err := run(os.Stdout, *controllerNs, *controllerName, *certFile, *printVersion, *validateSecret, reEncrypt, *dumpCert); err != nil { + if err := run(os.Stdout, *controllerNs, *controllerName, *certFile, *printVersion, *validateSecret, reEncrypt, *dumpCert, *mergeInto); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } diff --git a/cmd/kubeseal/main_test.go b/cmd/kubeseal/main_test.go index 1fd1cecd1..f88678299 100644 --- a/cmd/kubeseal/main_test.go +++ b/cmd/kubeseal/main_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "crypto/rsa" "fmt" "io" "io/ioutil" @@ -175,9 +176,134 @@ func TestSeal(t *testing.T) { // NB: See sealedsecret_test.go for e2e crypto test } +func mkTestSecret(t *testing.T, key, value string) []byte { + secret := v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testsecret", + Namespace: "testns", + Annotations: map[string]string{ + key: value, // putting secret here just to have a simple way to test annotation merges + }, + Labels: map[string]string{ + key: value, + }, + }, + Data: map[string][]byte{ + key: []byte(value), + }, + } + + info, ok := runtime.SerializerInfoForMediaType(scheme.Codecs.SupportedMediaTypes(), runtime.ContentTypeJSON) + if !ok { + t.Fatalf("binary can't serialize JSON") + } + enc := scheme.Codecs.EncoderForVersion(info.Serializer, v1.SchemeGroupVersion) + var inbuf bytes.Buffer + if err := enc.Encode(&secret, &inbuf); err != nil { + t.Fatalf("Error encoding: %v", err) + } + return inbuf.Bytes() +} + +func mkTestSealedSecret(t *testing.T, pubKey *rsa.PublicKey, key, value string) []byte { + inbuf := bytes.NewBuffer(mkTestSecret(t, key, value)) + var outbuf bytes.Buffer + if err := seal(inbuf, &outbuf, scheme.Codecs, pubKey); err != nil { + t.Fatalf("seal() returned error: %v", err) + } + + return outbuf.Bytes() +} + +func TestMergeInto(t *testing.T) { + pubKey, err := parseKey(strings.NewReader(testCert)) + if err != nil { + t.Fatalf("Failed to parse test key: %v", err) + } + + merge := func(newSecret, oldSealedSecret []byte) *ssv1alpha1.SealedSecret { + f, err := ioutil.TempFile("", "*.json") + if err != nil { + t.Fatal(err) + } + if _, err := f.Write(oldSealedSecret); err != nil { + t.Fatal(err) + } + f.Close() + + buf := bytes.NewBuffer(newSecret) + if err := sealMergingInto(buf, f.Name(), scheme.Codecs, pubKey); err != nil { + t.Fatal(err) + } + + b, err := ioutil.ReadFile(f.Name()) + if err != nil { + t.Fatal(err) + } + + merged, err := decodeSealedSecret(scheme.Codecs, b) + if err != nil { + t.Fatal(err) + } + return merged + } + + { + merged := merge( + mkTestSecret(t, "foo", "secret1"), + mkTestSealedSecret(t, pubKey, "bar", "secret2"), + ) + + checkAdded := func(m map[string]string, old, new string) { + if got, want := len(m), 2; got != want { + t.Fatalf("got: %d, want: %d", got, want) + } + + if _, ok := m[old]; !ok { + t.Fatalf("cannot find expected key") + } + + if _, ok := m[new]; !ok { + t.Fatalf("cannot find expected key") + } + } + + checkAdded(merged.Spec.EncryptedData, "foo", "bar") + checkAdded(merged.Spec.Template.Annotations, "foo", "bar") + checkAdded(merged.Spec.Template.Labels, "foo", "bar") + } + + { + origSrc := mkTestSealedSecret(t, pubKey, "foo", "secret2") + orig, err := decodeSealedSecret(scheme.Codecs, origSrc) + if err != nil { + t.Fatal(err) + } + + merged := merge( + mkTestSecret(t, "foo", "secret1"), + origSrc, + ) + + checkUpdated := func(before, after map[string]string, key string) { + if got, want := len(after), 1; got != want { + t.Fatalf("got: %d, want: %d", got, want) + } + + if old, new := before[key], after[key]; old == new { + t.Fatalf("expecting %q and %q to be different", old, new) + } + } + + checkUpdated(orig.Spec.EncryptedData, merged.Spec.EncryptedData, "foo") + checkUpdated(orig.Spec.Template.Annotations, merged.Spec.Template.Annotations, "foo") + checkUpdated(orig.Spec.Template.Labels, merged.Spec.Template.Labels, "foo") + } +} + func TestVersion(t *testing.T) { var buf strings.Builder - err := run(&buf, "", "", "", true, false, false, false) + err := run(&buf, "", "", "", true, false, false, false, "") if err != nil { t.Fatal(err) } @@ -189,7 +315,7 @@ func TestVersion(t *testing.T) { func TestMainError(t *testing.T) { const badFileName = "/?this/file/cannot/possibly/exist/can/it?" - err := run(ioutil.Discard, "", "", badFileName, false, false, false, false) + err := run(ioutil.Discard, "", "", badFileName, false, false, false, false, "") if err == nil || !os.IsNotExist(err) { t.Fatalf("expecting not exist error, got: %v", err)