Skip to content

Commit

Permalink
Merge #253
Browse files Browse the repository at this point in the history
253: Implement the --merge-into command r=mkmik a=mkmik

Addresses #95 although not by completely revamping the kubeseal syntax; I think that should be done more carefully while updating an existing secret is pretty high on the FAQ and yet I struggled to find a decent wording to intuitively explain how to manually merge the secrets so I just thought it's easier to just teach kubeseal how to do it.

This command also merges labels and annotations because I think it would surprise some people if it didn't.

Co-authored-by: Marko Mikulicic <[email protected]>
  • Loading branch information
bors[bot] and Marko Mikulicic authored Sep 12, 2019
2 parents 33016d5 + bdfaf84 commit d98c40d
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 4 deletions.
59 changes: 57 additions & 2 deletions cmd/kubeseal/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bytes"
"crypto/rsa"
"encoding/json"
"errors"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -345,14 +396,18 @@ 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)
}

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)
}
Expand Down
130 changes: 128 additions & 2 deletions cmd/kubeseal/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bytes"
"crypto/rsa"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down

0 comments on commit d98c40d

Please sign in to comment.