Repository: cloudflare/lockbox Branch: trunk Commit: b7bc038eb7db Files: 46 Total size: 128.7 KB Directory structure: gitextract_0jzthxzx/ ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── docker.yaml │ ├── semgrep.yml │ └── tests.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.org ├── cmd/ │ ├── lockbox-controller/ │ │ ├── Dockerfile │ │ ├── keypair.go │ │ └── main.go │ ├── lockbox-keypair/ │ │ └── main.go │ └── locket/ │ └── main.go ├── deployment/ │ ├── crds/ │ │ └── lockbox.k8s.cloudflare.com_lockboxes.yaml │ ├── manifests/ │ │ ├── deployment-lockbox.yaml │ │ ├── namespace-lockbox.yaml │ │ ├── service-lockbox.yaml │ │ └── serviceaccount-lockbox.yaml │ └── rbac/ │ ├── proxier.yaml │ ├── role-binding.yaml │ └── role.yaml ├── go.mod ├── go.sum ├── pkg/ │ ├── apis/ │ │ └── lockbox.k8s.cloudflare.com/ │ │ └── v1/ │ │ ├── groupversion_info.go │ │ ├── lockbox.go │ │ ├── lockbox_test.go │ │ ├── types.go │ │ └── zz_generated.deepcopy.go │ ├── flagvar/ │ │ ├── enum.go │ │ ├── enum_test.go │ │ ├── file.go │ │ ├── file_test.go │ │ ├── tcp_addr.go │ │ ├── tcp_addr_test.go │ │ └── testdata/ │ │ └── file │ ├── lockbox-controller/ │ │ ├── secretreconciler.go │ │ ├── secretreconciler_suite_test.go │ │ └── secretreconciler_test.go │ ├── lockbox-server/ │ │ └── serve.go │ ├── statemetrics/ │ │ ├── collector.go │ │ ├── handler.go │ │ ├── handler_test.go │ │ ├── labels.go │ │ └── labels_test.go │ └── util/ │ └── conditions/ │ └── conditions.go └── tools/ └── tools.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ bin/ ================================================ FILE: .github/workflows/docker.yaml ================================================ name: Docker on: - pull_request - push jobs: docker: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 - uses: docker/metadata-action@v5 id: docker-meta with: images: cloudflare/lockbox - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 if: ${{ startsWith(github.ref, 'refs/tags/v') }} with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - uses: docker/build-push-action@v5 with: file: ./cmd/lockbox-controller/Dockerfile platforms: linux/amd64, linux/arm64 tags: ${{ steps.docker-meta.outputs.tags }} push: ${{ startsWith(github.ref, 'refs/tags/v') }} ================================================ FILE: .github/workflows/semgrep.yml ================================================ on: pull_request: {} workflow_dispatch: {} push: branches: - trunk schedule: - cron: "0 0 * * *" name: Semgrep config jobs: semgrep: name: semgrep/ci runs-on: ubuntu-latest env: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} SEMGREP_URL: https://cloudflare.semgrep.dev SEMGREP_APP_URL: https://cloudflare.semgrep.dev SEMGREP_VERSION_CHECK_URL: https://cloudflare.semgrep.dev/api/check-version container: image: semgrep/semgrep steps: - uses: actions/checkout@v4 - run: semgrep ci ================================================ FILE: .github/workflows/tests.yaml ================================================ name: Test on: - pull_request - push jobs: unit: runs-on: ubuntu-latest name: "Go ${{ matrix.go }} Test" steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: go-version: "stable" - run: make test lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: go-version: "stable" - uses: dominikh/staticcheck-action@v1 with: build-tags: suite install-go: false integration: needs: - unit - lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: go-version: "stable" - run: | go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest source <(setup-envtest use -p env) go test ./... -tags suite ================================================ FILE: .gitignore ================================================ ## Go.gitignore ## # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Vendor directory /vendor ## nix.gitignore ## /result* /bin/ ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2020, Cloudflare, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ .DEFAULT_GOAL := binaries KERNEL := $(shell uname -s) GOTESTSUM := $(shell command -v gotestsum 2> /dev/null) DIB ?= docker IMAGE_ROOT ?= localhost/lockbox IMAGE_VERSION ?= $(shell git log -1 --pretty=format:%cd-%h --date short HEAD) VERSION := $(shell git describe --tags --always --dirty=-dev) # Build docker images for the native arch, but allow overriding in the environment for local development PLATFORM ?= local # Bind mount $SSL_CERT_FILE (or default) to build container if the file exists. SSL_CERT_FILE ?= /etc/ssl/certs/ca-certificates.crt ifneq (,$(wildcard ${SSL_CERT_FILE})) SECRETS = --secret id=certificates,src=${SSL_CERT_FILE} endif # When compiling for Linux enable Security's recommend hardening to satisfy `checksec' checks. # Unfortunately, most of these flags aren't portable to other operating systems. ifeq (${KERNEL},Linux) CGO_ENABLED ?= 1 CPPFLAGS ?= -D_FORTIFY_SOURCE=2 -fstack-protector-all CFLAGS ?= -O2 -pipe -fno-plt CXXFLAGS ?= -O2 -pipe -fno-plt LDFLAGS ?= -Wl,-O1,-sort-common,-as-needed,-z,relro,-z,now GO_LDFLAGS ?= -linkmode=external GOFLAGS ?= -buildmode=pie endif GO_LDFLAGS += -w -s -X main.version=${VERSION} GOFLAGS += -v export CGO_ENABLED export CGO_CPPFLAGS ?= ${CPPFLAGS} export CGO_CFLAGS ?= ${CFLAGS} export CGO_CXXFLAGS ?= ${CXXFLAGS} export CGO_LDFLAGS ?= ${LDFLAGS} CMDS := $(shell find cmd -mindepth 1 -maxdepth 1 -type d | awk -F '/' '{ print $$NF }' ) IMAGES := $(shell find cmd -mindepth 1 -type f -name Dockerfile | awk -F '/' '{ print $$2 }') define make-go-target .PHONY: bin/$1 bin/$1: go build ${GOFLAGS} -o $$@ -ldflags "${GO_LDFLAGS}" ./cmd/$1 endef define make-dib-targets .PHONY: images/$1 images/$1: ${DIB} buildx build --platform "$(PLATFORM)" ${SECRETS} -f cmd/$1/Dockerfile -t "${IMAGE_ROOT}/$1:${IMAGE_VERSION}" . .PHONY: push/images/$1 push/images/$1: ${DIB} push "${IMAGE_ROOT}/$1:${IMAGE_VERSION}" endef $(foreach element,$(CMDS), $(eval $(call make-go-target,$(element)))) $(foreach element,$(IMAGES), $(eval $(call make-dib-targets,$(element)))) .PHONY: binaries binaries: $(CMDS:%=bin/%) .PHONY: images images: $(IMAGES:%=images/%) .PHONY: push-images push-images: $(IMAGES:%=push/images/%) .PHONY: clean clean: rm -rf bin .PHONY: test test: ifdef GOTESTSUM "${GOTESTSUM}" -- -count 1 ./... else go test -cover -count 1 ./... endif .PHONY: lint lint: staticcheck -tags suite ./... .PHONY: controller-gen controller-gen: go install sigs.k8s.io/controller-tools/cmd/controller-gen .PHONY: go-generate go-generate: controller-gen go generate -v ./... ================================================ FILE: README.org ================================================ #+TITLE: Lockbox [[https://pkg.go.dev/github.com/cloudflare/lockbox][https://pkg.go.dev/badge/github.com/cloudflare/lockbox.png]] Lockbox is a secure way to store Kubernetes Secrets offline. Secrets are asymmetrically encrypted, and can only be decrypted by the Lockbox Kubernetes controller. A companion CLI tool, =locket=, makes encrypting secrets a one-step process. ** Features + Secure encryption using modern cryptography. Uses Salsa20, Poly1305, and Curve25519. + Secrets are locked to specific namespaces. + All Kubernetes Secret types are supported. + Plays nicely with Secrets created by other controllers. + Continuously reconciles child resources. ** Example Usage Create a native Secret, but pass =--dry-run= to avoid submitting to the API. #+begin_example $ kubectl create secret generic mysecret --namespace default \ --from-literal=foo=bar --dry-run -o yaml > mysecret.yaml #+end_example Then, use locket to encrypt the secret. #+begin_example $ locket -f mysecret.yaml > mylockbox.yaml #+end_example Submit the lockbox to the API. #+begin_example $ kubectl create -f mylockbox.yaml #+end_example Remove the unencrypted secret. #+begin_example $ rm mysecret.yaml #+end_example ================================================ FILE: cmd/lockbox-controller/Dockerfile ================================================ FROM docker.io/library/golang:1.21.5-bookworm AS builder WORKDIR /go/src/app ADD . /go/src/app RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ --mount=type=secret,id=certificates,target=/etc/ssl/certs/ca-certificates.crt \ make bin/lockbox-controller FROM gcr.io/distroless/base-nossl-debian12:nonroot COPY --from=builder /go/src/app/bin/lockbox-controller /bin ENTRYPOINT ["/bin/lockbox-controller"] ================================================ FILE: cmd/lockbox-controller/keypair.go ================================================ package main import ( "fmt" "io" "github.com/kevinburke/nacl" "sigs.k8s.io/yaml" ) type kp struct { Private []byte `json:"private"` Public []byte `json:"public"` } // KeyPairFromYAMLOrJSON loads a public/private NaCL keypair from a YAML or JSON file. func KeyPairFromYAMLOrJSON(r io.Reader) (pub, pri nacl.Key, err error) { data, err := io.ReadAll(r) if err != nil { return } keypair := kp{} err = yaml.Unmarshal(data, &keypair, yaml.DisallowUnknownFields) if err != nil { return } if len(keypair.Private) != 32 { err = fmt.Errorf("incorrect private key length: %d, should be 32", len(keypair.Private)) return } if len(keypair.Public) != 32 { err = fmt.Errorf("incorrect public key length: %d, should be 32", len(keypair.Public)) return } pub = new([nacl.KeySize]byte) pri = new([nacl.KeySize]byte) copy(pri[:], keypair.Private) copy(pub[:], keypair.Public) return } ================================================ FILE: cmd/lockbox-controller/main.go ================================================ package main import ( "context" "flag" "fmt" "net" "net/http" "os" "runtime" "time" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" "github.com/cloudflare/lockbox/pkg/flagvar" lockboxcontroller "github.com/cloudflare/lockbox/pkg/lockbox-controller" server "github.com/cloudflare/lockbox/pkg/lockbox-server" "github.com/cloudflare/lockbox/pkg/statemetrics" "github.com/go-logr/zerologr" "github.com/kevinburke/nacl" "github.com/rs/zerolog" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/manager/signals" "sigs.k8s.io/controller-runtime/pkg/metrics" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) var ( pubKey, priKey nacl.Key version = "dev" syncPeriod = 1 * time.Hour keypairPath = flagvar.File{Value: "/etc/lockbox/keypair.yaml"} metricsAddr = flagvar.TCPAddr{Text: ":8080"} httpAddr = flagvar.TCPAddr{Text: ":8081"} ) func main() { flag.Var(&keypairPath, "keypair", fmt.Sprintf("public/private 32 byte keypairs (%s)", keypairPath.Help())) flag.Var(&metricsAddr, "metrics-addr", fmt.Sprintf("bind for HTTP metrics (%s)", metricsAddr.Help())) flag.Var(&httpAddr, "http-addr", fmt.Sprintf("bind for HTTP server (%s)", httpAddr.Help())) flag.DurationVar(&syncPeriod, "sync-period", syncPeriod, "controller sync period") flag.String("v", "", "log level for V logs") flag.Parse() zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs zerologr.NameFieldName = "logger" zerologr.NameSeparator = "/" zl := zerolog.New(os.Stderr).With().Caller().Timestamp().Logger() logf.SetLogger(zerologr.New(&zl)) logger := zl.With().Str("name", "main").Logger() keypair, err := os.Open(keypairPath.Value) if err != nil { logger.Fatal().Err(err).Str("path", keypairPath.Value).Msg("unable to open keypair") os.Exit(1) } pubKey, priKey, err = KeyPairFromYAMLOrJSON(keypair) if err != nil { logger.Fatal().Err(err).Str("path", keypairPath.Value).Msg("unable to parse keypair") os.Exit(1) } keypair.Close() err = lockboxv1.AddToScheme(scheme.Scheme) if err != nil { logger.Fatal().Err(err).Msg("unable to add lockbox schemes") os.Exit(1) } cfg, err := config.GetConfig() cfg.UserAgent = fmt.Sprintf("%s/%s (%s/%s)", os.Args[0], version, runtime.GOOS, runtime.GOARCH) if err != nil { logger.Fatal().Err(err).Msg("unable to get kubeconfig") os.Exit(1) } mgr, err := manager.New(cfg, manager.Options{ Metrics: metricsserver.Options{ BindAddress: metricsAddr.Text, }, Cache: cache.Options{ SyncPeriod: &syncPeriod, }, Scheme: scheme.Scheme, }) if err != nil { logger.Fatal().Err(err).Msg("unable to create controller manager") os.Exit(1) } recorder := mgr.GetEventRecorderFor("lockbox") client := mgr.GetClient() sr := lockboxcontroller.NewSecretReconciler(pubKey, priKey, lockboxcontroller.WithRecorder(recorder), lockboxcontroller.WithClient(client)) info := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ Name: "kube_lockbox_info", Help: "Information about Lockbox", }, []string{"namespace", "lockbox"}) created := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ Name: "kube_lockbox_created", Help: "Unix creation timestamp", }, []string{"namespace", "lockbox"}) resourceVersion := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ Name: "kube_lockbox_resource_version", Help: "Resource version representing a specific version of a Lockbox", }, []string{"namespace", "lockbox", "resource_version"}) lbType := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ Name: "kube_lockbox_type", Help: "Lockbox secret type", }, []string{"namespace", "lockbox", "type"}) peerKey := statemetrics.NewKubernetesVec(statemetrics.KubernetesOpts{ Name: "kube_lockbox_peer", Help: "Lockbox peer key", }, []string{"namespace", "lockbox", "peer"}) labels := statemetrics.NewLabelsVec(statemetrics.KubernetesOpts{ Name: "kube_lockbox_labels", Help: "Kubernetes labels converted to Prometheus labels", }) metrics.Registry.MustRegister(info, created, resourceVersion, lbType, labels, peerKey) mh := statemetrics.NewStateMetricProxy( &handler.EnqueueRequestForObject{}, info, created, resourceVersion, lbType, peerKey, labels, ) c, err := controller.New("lockbox-controller", mgr, controller.Options{ Reconciler: reconcile.AsReconciler(mgr.GetClient(), sr), }) if err != nil { logger.Fatal().Err(err).Msg("unable to create controller") os.Exit(1) } if err := c.Watch(source.Kind(mgr.GetCache(), &lockboxv1.Lockbox{}), mh); err != nil { logger.Fatal().Err(err).Msg("unable to watch Lockbox resources") os.Exit(1) } if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Secret{}), handler.EnqueueRequestForOwner(scheme.Scheme, mgr.GetRESTMapper(), &lockboxv1.Lockbox{}, handler.OnlyControllerOwner())); err != nil { logger.Fatal().Err(err).Msg("unable to watch Secret resources") os.Exit(1) } // TODO(terin): make server implement Runnable if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error { mux := http.NewServeMux() mux.Handle("/v1/public", server.PublicKey(pubKey)) ln, err := net.Listen("tcp", httpAddr.Text) if err != nil { return err } // sig.kubernetes.io/controller-runtime/pkg/internal/httpserver s := http.Server{ Handler: mux, MaxHeaderBytes: 1 << 20, IdleTimeout: 90 * time.Second, ReadHeaderTimeout: 32 * time.Second, } idleConnsClosed := make(chan struct{}) go func() { <-ctx.Done() if err := s.Shutdown(context.Background()); err != nil { logger.Err(err).Send() } close(idleConnsClosed) }() if err := s.Serve(ln); err != nil && err != http.ErrServerClosed { return err } <-idleConnsClosed return nil })); err != nil { logger.Fatal().Err(err).Msg("unable to add server runnable") } if err := mgr.Start(signals.SetupSignalHandler()); err != nil { logger.Fatal().Err(err).Send() } } ================================================ FILE: cmd/lockbox-keypair/main.go ================================================ package main import ( "crypto/rand" "encoding/base64" "fmt" "os" "github.com/kevinburke/nacl/box" ) func main() { lockboxPubKey, lockboxPriKey, err := box.GenerateKey(rand.Reader) if err != nil { panic(err) } pub64 := base64.StdEncoding.EncodeToString(lockboxPubKey[:]) pri64 := base64.StdEncoding.EncodeToString(lockboxPriKey[:]) fmt.Fprintf(os.Stdout, "public: %s\nprivate: %s\n", pub64, pri64) } ================================================ FILE: cmd/locket/main.go ================================================ package main import ( "context" "crypto/rand" "flag" "fmt" "io" "os" gruntime "runtime" "time" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" "github.com/cloudflare/lockbox/pkg/flagvar" "github.com/go-logr/zerologr" "github.com/kevinburke/nacl" "github.com/kevinburke/nacl/box" "github.com/rs/zerolog" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" logf "sigs.k8s.io/controller-runtime/pkg/log" ) var ( input = flagvar.File{} kubeconfig = flagvar.File{} output = flagvar.Enum{Choices: []string{"json", "yaml"}, Value: "yaml"} version = "dev" printVersion bool peerHex string masterURL string lockboxNS string lockboxSvc string ) func main() { flag.Var(&input, "f", fmt.Sprintf("input file (%s)", input.Help())) flag.Var(&output, "o", fmt.Sprintf("output format (%s)", output.Help())) flag.Var(&kubeconfig, "kubeconfig", fmt.Sprintf("path to kubeconfig. (%s)", kubeconfig.Help())) flag.StringVar(&peerHex, "peer-hex", "", "peer public key (32-bit hex)") flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") flag.StringVar(&lockboxNS, "lockbox-namespace", "lockbox", "namespace of the lockbox controller") flag.StringVar(&lockboxSvc, "lockbox-service", "lockbox", "name of the lockbox service") flag.BoolVar(&printVersion, "version", false, "print version") flag.String("v", "", "log level for V logs") flag.Parse() ctx := context.Background() if printVersion { fmt.Printf("locket: %s\n", version) os.Exit(0) } zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs zerologr.NameFieldName = "logger" zerologr.NameSeparator = "/" zl := zerolog.New(os.Stderr).With().Caller().Timestamp().Logger() logf.SetLogger(zerologr.New(&zl)) logger := zl.With().Str("name", "main").Logger() err := lockboxv1.AddToScheme(scheme.Scheme) if err != nil { logger.Fatal().Err(err).Msg("unable to add lockbox schemes") os.Exit(1) } var r io.Reader if input.String() == "" { r = os.Stdin } else { r, err = os.Open(input.String()) if err != nil { logger.Fatal().Err(err).Msg("unable to open secret file") os.Exit(1) } } w := os.Stdout cfg := GetConfig() cf := runtimeserializer.NewCodecFactory(scheme.Scheme) ib, err := io.ReadAll(r) if err != nil { logger.Fatal().Err(err).Msg("unable to read secret file") os.Exit(1) } var secret corev1.Secret if err = runtime.DecodeInto(cf.UniversalDecoder(), ib, &secret); err != nil { logger.Fatal().Err(err).Msg("unable to decode secret file") os.Exit(1) } pubKey, priKey, err := box.GenerateKey(rand.Reader) if err != nil { logger.Fatal().Err(err).Msg("could not generate key") os.Exit(1) } var peerKey nacl.Key switch peerHex { case "": ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() cc, err := cfg.ClientConfig() if err != nil { logger.Fatal().Err(err).Msg("unable to create API client configuration") os.Exit(1) } cc.UserAgent = fmt.Sprintf("%s/%s (%s/%s)", os.Args[0], version, gruntime.GOOS, gruntime.GOARCH) client, err := kubernetes.NewForConfig(cc) if err != nil { logger.Fatal().Err(err).Msg("unable to create API client") os.Exit(1) } b, err := GetRemotePublicKey(ctx, client, lockboxNS, lockboxSvc) if err != nil { logger.Fatal().Err(err).Msg("unable to fetch public key") os.Exit(1) } if len(b) != 32 { err = fmt.Errorf("incorrect peer key length: %d, should be 32", len(b)) logger.Fatal().Err(err).Msg("unable to fetch peer key") return } peerKey = new([nacl.KeySize]byte) copy(peerKey[:], b) default: peerKey, err = nacl.Load(peerHex) if err != nil { logger.Fatal().Err(err).Msg("could not load --peer-hex") os.Exit(1) } } namespace := secret.Namespace if namespace == "" { namespace, _, _ = cfg.Namespace() } b := lockboxv1.NewFromSecret(secret, namespace, peerKey, pubKey, priKey) var ct string switch output.String() { case "yaml": ct = "application/yaml" case "json": ct = "application/json" } info, ok := runtime.SerializerInfoForMediaType(cf.SupportedMediaTypes(), ct) if !ok { logger.Fatal().Str("content-type", ct).Msg("can't serialize to content-type") os.Exit(1) } serial := info.Serializer if info.PrettySerializer != nil { serial = info.PrettySerializer } enc := cf.EncoderForVersion(serial, lockboxv1.GroupVersion) ob, err := runtime.Encode(enc, b) if err != nil { logger.Fatal().Err(err).Msg("unable to encode Lockbox") os.Exit(1) } if _, err := w.Write(ob); err != nil { logger.Fatal().Err(err).Send() } if _, err := w.WriteString("\n"); err != nil { logger.Fatal().Err(err).Send() } } func GetConfig() clientcmd.ClientConfig { loader := clientcmd.NewDefaultClientConfigLoadingRules() overrides := clientcmd.ConfigOverrides{ ClusterInfo: clientcmdapi.Cluster{ Server: masterURL, }, } loader.ExplicitPath = kubeconfig.String() return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, &overrides) } func GetRemotePublicKey(ctx context.Context, c kubernetes.Interface, ns, svc string) ([]byte, error) { return c.CoreV1().Services(ns).ProxyGet("http", svc, "", "/v1/public", nil).DoRaw(ctx) } ================================================ FILE: deployment/crds/lockbox.k8s.cloudflare.com_lockboxes.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 name: lockboxes.lockbox.k8s.cloudflare.com spec: group: lockbox.k8s.cloudflare.com names: kind: Lockbox listKind: LockboxList plural: lockboxes singular: lockbox scope: Namespaced versions: - additionalPrinterColumns: - jsonPath: .spec.template.type name: SecretType type: string - jsonPath: .spec.peer name: Peer type: string name: v1 schema: openAPIV3Schema: description: Lockbox is a struct wrapping the LockboxSpec in standard API server metadata fields. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object spec: description: Desired state of the Lockbox resource. properties: data: additionalProperties: format: byte type: string description: Data contains the secret data, encrypted to the Peer's public key. Each key in the data map must consist of alphanumeric characters, '-', '_', or '.'. type: object namespace: description: Namespace stores an encrypted copy of which namespace this Lockbox is locked for, ensuring it cannot be deployed to another namespace under an attacker's control. format: byte type: string peer: description: Peer stores the public key that can unlock this Lockbox. format: byte type: string sender: description: Sender stores the public key used to lock this Lockbox. format: byte type: string template: description: Template defines the structure of the Secret that will be created from this Lockbox. properties: metadata: properties: annotations: additionalProperties: type: string description: 'Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations' type: object labels: additionalProperties: type: string description: 'Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels' type: object type: object type: description: Type is used to facilitate programmatic handling of secret data. type: string type: object required: - data - namespace - peer - sender type: object status: description: Status of the Lockbox. This is set and managed automatically. properties: conditions: description: List of status conditions to indicate the status of a Lockbox. items: description: Condition contains condition information for a Lockbox. properties: lastTransitionTime: description: LastTransitionTime marks when the condition last transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: A message is the human readable message indicating details about the transition. The field may be empty. type: string reason: description: The reason for the condition's last transition in CamelCase. type: string severity: description: Severity provides explicit classification of Reason code, so that users or machines can immediately understand the current situation and act accordingly. The Severity field MUST be set only when Status=False. enum: - Error - Warning - Info type: string status: description: Status of the condition, one of True, False, Unknown type: string type: description: Type of condition in CamelCase. enum: - Ready type: string required: - status - type type: object type: array type: object required: - spec type: object served: true storage: true subresources: status: {} ================================================ FILE: deployment/manifests/deployment-lockbox.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: lockbox-controller namespace: lockbox spec: replicas: 1 selector: matchLabels: app: lockbox component: controller template: metadata: labels: app: lockbox component: controller spec: serviceAccountName: lockbox-controller containers: - name: lockbox image: cloudflare/lockbox:v0.6.0 ports: - containerPort: 8080 name: http-metrics - containerPort: 8081 name: http-api volumeMounts: - name: keypair mountPath: /etc/lockbox/ readOnly: true volumes: - name: keypair secret: secretName: keypair defaultMode: 256 ================================================ FILE: deployment/manifests/namespace-lockbox.yaml ================================================ apiVersion: v1 kind: Namespace metadata: name: lockbox ================================================ FILE: deployment/manifests/service-lockbox.yaml ================================================ kind: Service apiVersion: v1 metadata: name: lockbox namespace: lockbox spec: ports: - port: 80 targetPort: 8081 selector: app: lockbox component: controller ================================================ FILE: deployment/manifests/serviceaccount-lockbox.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: name: lockbox-controller namespace: lockbox ================================================ FILE: deployment/rbac/proxier.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: lockbox-proxier namespace: lockbox rules: - apiGroups: - "" resources: - "services/proxy" resourceNames: - "http:lockbox:" verbs: - "get" --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: lockbox-proxier namespace: lockbox roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: lockbox-proxier subjects: - kind: Group name: system:authenticated ================================================ FILE: deployment/rbac/role-binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: lockbox-controller roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: lockbox-controller subjects: - kind: ServiceAccount name: lockbox-controller namespace: lockbox ================================================ FILE: deployment/rbac/role.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: lockbox-controller rules: - apiGroups: - "" resources: - events verbs: - create - patch - apiGroups: - "" resources: - secrets verbs: - create - get - list - patch - update - watch - apiGroups: - lockbox.k8s.cloudflare.com resources: - lockboxes verbs: - get - list - watch - apiGroups: - lockbox.k8s.cloudflare.com resources: - lockboxes/status verbs: - get - patch - update ================================================ FILE: go.mod ================================================ module github.com/cloudflare/lockbox go 1.21 toolchain go1.21.5 require ( github.com/go-logr/zerologr v1.2.3 github.com/google/go-cmp v0.6.0 github.com/kevinburke/nacl v0.0.0-20210405173606-cd9060f5f776 github.com/prometheus/client_golang v1.18.0 github.com/prometheus/common v0.45.0 github.com/rs/zerolog v1.29.1 gotest.tools/v3 v3.4.0 k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 k8s.io/client-go v0.29.0 k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/controller-runtime v0.17.0 sigs.k8s.io/controller-tools v0.13.0 sigs.k8s.io/yaml v1.4.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.8.0 // indirect github.com/fatih/color v1.15.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gobuffalo/flect v1.0.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.16.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.12.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/term v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.16.1 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect k8s.io/component-base v0.29.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/nacl v0.0.0-20210405173606-cd9060f5f776 h1:W8T7zJRO9imecUZySwPkuXHosjp2MloqAY1eSAEEOIo= github.com/kevinburke/nacl v0.0.0-20210405173606-cd9060f5f776/go.mod h1:VUp2yfq+wAk8hMl3NNN34fXjzUD9xMpGvUL8eSJz9Ns= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.17.0 h1:fjJQf8Ukya+VjogLO6/bNX9HE6Y2xpsO5+fyS26ur/s= sigs.k8s.io/controller-runtime v0.17.0/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= ================================================ FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/groupversion_info.go ================================================ // +kubebuilder:object:generate=true // +groupName=lockbox.k8s.cloudflare.com // Package v1 is the v1 version of the Lockbox API package v1 import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) //go:generate controller-gen object crd paths=./. output:crd:artifacts:config=../../../../deployment/crds var ( // GroupVersion is group version used to register these objects GroupVersion = schema.GroupVersion{Group: "lockbox.k8s.cloudflare.com", Version: "v1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) func init() { SchemeBuilder.Register(&Lockbox{}, &LockboxList{}) } ================================================ FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox.go ================================================ package v1 import ( "github.com/kevinburke/nacl" "github.com/kevinburke/nacl/box" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const keySize = nacl.KeySize // NewFromSecret creates a Lockbox wrapping the provided Secret. The value of each secret // are individually encrypted using the provided key pair. func NewFromSecret(secret corev1.Secret, namespace string, peer, pub, pri nacl.Key) *Lockbox { encNS := box.EasySeal([]byte(namespace), peer, pri) b := &Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: secret.Name, Namespace: namespace, }, Spec: LockboxSpec{ Sender: pub[:], Peer: peer[:], Namespace: encNS, Data: map[string][]byte{}, Template: LockboxSecretTemplate{ LockboxSecretTemplateMetadata: LockboxSecretTemplateMetadata{ Labels: secret.ObjectMeta.Labels, Annotations: secret.ObjectMeta.Annotations, }, Type: secret.Type, }, }, } for key, value := range secret.Data { enc := box.EasySeal(value, peer, pri) b.Spec.Data[key] = enc } for key, value := range secret.StringData { enc := box.EasySeal([]byte(value), peer, pri) b.Spec.Data[key] = enc } return b } // UnlockInto decrypts each secret value into the provided secret. func (in *Lockbox) UnlockInto(secret *corev1.Secret, pri nacl.Key) error { sender := new([keySize]byte) copy(sender[:], in.Spec.Sender) data := make(map[string][]byte, len(in.Spec.Data)) for key, val := range in.Spec.Data { d, err := box.EasyOpen(val, sender, pri) if err != nil { return decryptSecretKeyError{error: err, key: key} } data[key] = d } secret.Data = data secret.Type = in.Spec.Template.Type secret.Labels = in.Spec.Template.Labels secret.Annotations = in.Spec.Template.Annotations return nil } // decryptSecretKeyError wraps error while decrypting data from a secret. // This allows preserving the key for farther error messages. type decryptSecretKeyError struct { error key string } // SecretKey returns the secret data key that triggered this error. func (e decryptSecretKeyError) SecretKey() string { return e.key } // Unwrap implements Wrapper, returning the underlying error message. func (e decryptSecretKeyError) Unwrap() error { return e.error } func (in *Lockbox) GetConditions() []Condition { return in.Status.Conditions } func (in *Lockbox) SetConditions(conditions []Condition) { in.Status.Conditions = conditions } ================================================ FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox_test.go ================================================ package v1_test import ( "crypto/rand" "testing" v1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" "github.com/kevinburke/nacl" "github.com/kevinburke/nacl/box" "gotest.tools/v3/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestUnlock(t *testing.T) { _, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") assert.NilError(t, err) lb := &v1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", Labels: map[string]string{ "type": "lockbox", }, Annotations: map[string]string{ "helm.sh/hook": "pre-install", }, }, Spec: v1.LockboxSpec{ Sender: []byte{0xb2, 0xa3, 0xf, 0x85, 0xa, 0x58, 0xcf, 0x94, 0x4c, 0x62, 0x37, 0xd4, 0xef, 0xf5, 0xed, 0x11, 0x52, 0xfa, 0x1b, 0xc3, 0xb0, 0x4d, 0x27, 0xd5, 0x58, 0x67, 0x61, 0x67, 0xe0, 0x10, 0xb1, 0x5c}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0x4d, 0xa0, 0x73, 0x8b, 0x95, 0xc3, 0xd4, 0x64, 0xe9, 0xab, 0xd, 0xb7, 0x1e, 0x5, 0x10, 0xed, 0x4c, 0x2f, 0x8a, 0x66, 0x6d, 0xec, 0x7c, 0x5d, 0x9b, 0xa7, 0xb7, 0x88, 0x49, 0x8a, 0xb9, 0x7f, 0xf0, 0x30, 0xe0, 0xad, 0x49, 0x7c, 0x3f, 0xe3, 0x1c, 0x2e, 0xe9, 0xb1, 0x2a, 0x70, 0x28}, Template: v1.LockboxSecretTemplate{ LockboxSecretTemplateMetadata: v1.LockboxSecretTemplateMetadata{ Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, }, }, Data: map[string][]byte{ "test": {0x7b, 0xca, 0x32, 0x90, 0xf7, 0x97, 0x3b, 0x6, 0xfb, 0x7c, 0xdc, 0x3a, 0x25, 0x82, 0x29, 0xdf, 0x9d, 0x1e, 0x46, 0x8d, 0xd4, 0x99, 0x49, 0x2, 0x63, 0x56, 0x54, 0x64, 0xae, 0x9e, 0xf2, 0xc0, 0x35, 0xf5, 0xf1, 0xcb, 0x67, 0xb7, 0xe2, 0xb1, 0x14, 0x42, 0x71, 0xc}, "test1": {0x2c, 0x68, 0xed, 0x53, 0x55, 0x55, 0xe2, 0x2d, 0x71, 0x96, 0x85, 0xfd, 0xdb, 0x93, 0x1e, 0x77, 0x91, 0x2d, 0x76, 0xba, 0xae, 0x46, 0x30, 0x9e, 0xb6, 0x65, 0xa2, 0x49, 0xfe, 0x78, 0xc0, 0xcb, 0x6d, 0xf, 0xa8, 0xeb, 0xa8, 0xfc, 0xc0, 0xa0, 0xdc, 0x4, 0x16, 0x7, 0xa0}, }, }, } secret := &corev1.Secret{} expected := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, }, Data: map[string][]byte{ "test": {0x74, 0x65, 0x73, 0x74}, "test1": {0x74, 0x65, 0x73, 0x74, 0x31}, }, } assert.NilError(t, lb.UnlockInto(secret, priKey)) assert.DeepEqual(t, secret, expected) } func TestUnlockErr(t *testing.T) { _, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") assert.NilError(t, err) lb := &v1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", Labels: map[string]string{ "type": "lockbox", }, Annotations: map[string]string{ "helm.sh/hook": "pre-install", }, }, Spec: v1.LockboxSpec{ Sender: []byte{0x46, 0xa5, 0xfa, 0xa9, 0xe1, 0xe6, 0xd8, 0x48, 0x14, 0xfd, 0x52, 0x67, 0x98, 0x39, 0x12, 0xda, 0x78, 0x61, 0x95, 0x84, 0x8d, 0xbb, 0xd2, 0x1f, 0x36, 0xaa, 0xdc, 0x6a, 0x18, 0x5c, 0xe5, 0x77}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x01, 0x1f, 0xb8, 0x8c, 0x01, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0x0d, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0x4d, 0xa0, 0x73, 0x8b, 0x95, 0xc3, 0xd4, 0x64, 0xe9, 0xab, 0xd, 0xb7, 0x1e, 0x5, 0x10, 0xed, 0x4c, 0x2f, 0x8a, 0x66, 0x6d, 0xec, 0x7c, 0x5d, 0x9b, 0xa7, 0xb7, 0x88, 0x49, 0x8a, 0xb9, 0x7f, 0xf0, 0x30, 0xe0, 0xad, 0x49, 0x7c, 0x3f, 0xe3, 0x1c, 0x2e, 0xe9, 0xb1, 0x2a, 0x70, 0x28}, Template: v1.LockboxSecretTemplate{ LockboxSecretTemplateMetadata: v1.LockboxSecretTemplateMetadata{ Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, }, }, Data: map[string][]byte{ "test": {0x7b, 0xca, 0x32, 0x90, 0xf7, 0x97, 0x3b, 0x6, 0xfb, 0x7c, 0xdc, 0x3a, 0x25, 0x82, 0x29, 0xdf, 0x9d, 0x1e, 0x46, 0x8d, 0xd4, 0x99, 0x49, 0x2, 0x63, 0x56, 0x54, 0x64, 0xae, 0x9e, 0xf2, 0xc0, 0x35, 0xf5, 0xf1, 0xcb, 0x67, 0xb7, 0xe2, 0xb1, 0x14, 0x42, 0x71, 0xc}, "test1": {0x2c, 0x68, 0xed, 0x53, 0x55, 0x55, 0xe2, 0x2d, 0x71, 0x96, 0x85, 0xfd, 0xdb, 0x93, 0x1e, 0x77, 0x91, 0x2d, 0x76, 0xba, 0xae, 0x46, 0x30, 0x9e, 0xb6, 0x65, 0xa2, 0x49, 0xfe, 0x78, 0xc0, 0xcb, 0x6d, 0xf, 0xa8, 0xeb, 0xa8, 0xfc, 0xc0, 0xa0, 0xdc, 0x4, 0x16, 0x7, 0xa0}, }, }, } secret := &corev1.Secret{} expected := &corev1.Secret{} err = lb.UnlockInto(secret, priKey) assert.ErrorContains(t, err, "Could not decrypt invalid input") assert.DeepEqual(t, secret, expected) } func TestLockUnlock(t *testing.T) { senderPubKey, senderPriKey, _ := box.GenerateKey(rand.Reader) serverPubKey, serverPriKey, _ := box.GenerateKey(rand.Reader) secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "type": "secret", }, }, Data: map[string][]byte{ "test": {0x74, 0x65, 0x73, 0x74}, }, } lb := v1.NewFromSecret(secret, "namespace", serverPubKey, senderPubKey, senderPriKey) unlockedSecret := &corev1.Secret{} expectedSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "type": "secret", }, }, Data: map[string][]byte{ "test": {0x74, 0x65, 0x73, 0x74}, }, } assert.NilError(t, lb.UnlockInto(unlockedSecret, serverPriKey)) assert.DeepEqual(t, unlockedSecret, expectedSecret) } func loadKeypair(t *testing.T, pub, pri string) (pubKey, priKey nacl.Key, err error) { t.Helper() pubKey, err = nacl.Load(pub) if err != nil { return } priKey, err = nacl.Load(pri) return } ================================================ FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/types.go ================================================ package v1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="SecretType",type=string,JSONPath=`.spec.template.type` // +kubebuilder:printcolumn:name="Peer",type=string,JSONPath=`.spec.peer` // Lockbox is a struct wrapping the LockboxSpec in standard API server // metadata fields. type Lockbox struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` // Desired state of the Lockbox resource. Spec LockboxSpec `json:"spec"` // Status of the Lockbox. This is set and managed automatically. // +optional Status LockboxStatus `json:"status,omitempty"` } // LockboxSpec is a struct wrapping the encrypted secrets along with the // public keys of the sender and server. type LockboxSpec struct { // Sender stores the public key used to lock this Lockbox. Sender []byte `json:"sender"` // Peer stores the public key that can unlock this Lockbox. Peer []byte `json:"peer"` // Namespace stores an encrypted copy of which namespace this Lockbox is locked // for, ensuring it cannot be deployed to another namespace under an attacker's // control. Namespace []byte `json:"namespace"` // Data contains the secret data, encrypted to the Peer's public key. Each key in the // data map must consist of alphanumeric characters, '-', '_', or '.'. Data map[string][]byte `json:"data"` // Template defines the structure of the Secret that will be // created from this Lockbox. // +optional Template LockboxSecretTemplate `json:"template,omitempty"` } // LockboxSecretTemplate defines structure of API metadata fields // of Secrets controlled by a Lockbox. type LockboxSecretTemplate struct { LockboxSecretTemplateMetadata `json:"metadata,omitempty"` // Type is used to facilitate programmatic handling of secret data. Type corev1.SecretType `json:"type,omitempty"` } type LockboxSecretTemplateMetadata struct { // Map of string keys and values that can be used to organize and categorize // (scope and select) objects. May match selectors of replication // controllers and services. More info: // http://kubernetes.io/docs/user-guide/labels // +optional Labels map[string]string `json:"labels,omitempty"` // Annotations is an unstructured key value map stored with a resource that // may be set by external tools to store and retrieve arbitrary metadata. // They are not queryable and should be preserved when modifying objects. // More info: http://kubernetes.io/docs/user-guide/annotations // +optional Annotations map[string]string `json:"annotations,omitempty"` } // LockboxStatus contains status information about a Lockbox. type LockboxStatus struct { // List of status conditions to indicate the status of a Lockbox. // +optional Conditions []Condition `json:"conditions,omitempty"` } // Condition contains condition information for a Lockbox. type Condition struct { // Type of condition in CamelCase. // +required Type ConditionType `json:"type"` // Status of the condition, one of True, False, Unknown // +required Status corev1.ConditionStatus `json:"status"` // Severity provides explicit classification of Reason code, so that users or machines // can immediately understand the current situation and act accordingly. // The Severity field MUST be set only when Status=False. // +optional Severity ConditionSeverity `json:"severity"` // LastTransitionTime marks when the condition last transitioned from one status to another. // This should be when the underlying condition changed. If that is not known, then using the time // when the API field changed is acceptable. // +required LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` // The reason for the condition's last transition in CamelCase. // +optional Reason string `json:"reason,omitempty"` // A message is the human readable message indicating details about the transition. // The field may be empty. // +optional Message string `json:"message,omitempty"` } // +kubebuilder:validation:Enum=Ready type ConditionType string const ( ReadyCondition ConditionType = "Ready" ) // +kubebuilder:validation:Enum=Error;Warning;Info type ConditionSeverity string const ( ConditionSeverityError ConditionSeverity = "Error" ConditionSeverityWarning ConditionSeverity = "Warning" ConditionSeverityInfo ConditionSeverity = "Info" ConditionSeverityNone ConditionSeverity = "" ) // +kubebuilder:object:root=true // LockboxList is a Lockbox-specific version of metav1.List. type LockboxList struct { metav1.TypeMeta metav1.ListMeta Items []Lockbox } ================================================ FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated // Code generated by controller-gen. DO NOT EDIT. package v1 import ( runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. func (in *Condition) DeepCopy() *Condition { if in == nil { return nil } out := new(Condition) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Lockbox) DeepCopyInto(out *Lockbox) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lockbox. func (in *Lockbox) DeepCopy() *Lockbox { if in == nil { return nil } out := new(Lockbox) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Lockbox) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LockboxList) DeepCopyInto(out *LockboxList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Lockbox, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxList. func (in *LockboxList) DeepCopy() *LockboxList { if in == nil { return nil } out := new(LockboxList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *LockboxList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LockboxSecretTemplate) DeepCopyInto(out *LockboxSecretTemplate) { *out = *in in.LockboxSecretTemplateMetadata.DeepCopyInto(&out.LockboxSecretTemplateMetadata) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxSecretTemplate. func (in *LockboxSecretTemplate) DeepCopy() *LockboxSecretTemplate { if in == nil { return nil } out := new(LockboxSecretTemplate) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LockboxSecretTemplateMetadata) DeepCopyInto(out *LockboxSecretTemplateMetadata) { *out = *in if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxSecretTemplateMetadata. func (in *LockboxSecretTemplateMetadata) DeepCopy() *LockboxSecretTemplateMetadata { if in == nil { return nil } out := new(LockboxSecretTemplateMetadata) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LockboxSpec) DeepCopyInto(out *LockboxSpec) { *out = *in if in.Sender != nil { in, out := &in.Sender, &out.Sender *out = make([]byte, len(*in)) copy(*out, *in) } if in.Peer != nil { in, out := &in.Peer, &out.Peer *out = make([]byte, len(*in)) copy(*out, *in) } if in.Namespace != nil { in, out := &in.Namespace, &out.Namespace *out = make([]byte, len(*in)) copy(*out, *in) } if in.Data != nil { in, out := &in.Data, &out.Data *out = make(map[string][]byte, len(*in)) for key, val := range *in { var outVal []byte if val == nil { (*out)[key] = nil } else { inVal := (*in)[key] in, out := &inVal, &outVal *out = make([]byte, len(*in)) copy(*out, *in) } (*out)[key] = outVal } } in.Template.DeepCopyInto(&out.Template) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxSpec. func (in *LockboxSpec) DeepCopy() *LockboxSpec { if in == nil { return nil } out := new(LockboxSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LockboxStatus) DeepCopyInto(out *LockboxStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LockboxStatus. func (in *LockboxStatus) DeepCopy() *LockboxStatus { if in == nil { return nil } out := new(LockboxStatus) in.DeepCopyInto(out) return out } ================================================ FILE: pkg/flagvar/enum.go ================================================ package flagvar import ( "errors" "fmt" "strings" ) var ErrInvalidEnum = errors.New("invalid enum option") type Enum struct { Choices []string Value string } func (e *Enum) Help() string { return fmt.Sprintf("one of %v", e.Choices) } func (e *Enum) Set(v string) error { for _, c := range e.Choices { if strings.EqualFold(c, v) { e.Value = strings.ToLower(v) return nil } } return ErrInvalidEnum } func (e *Enum) String() string { if e == nil { return "" } return e.Value } ================================================ FILE: pkg/flagvar/enum_test.go ================================================ package flagvar_test import ( "testing" "github.com/cloudflare/lockbox/pkg/flagvar" "gotest.tools/v3/assert" ) func TestEnumString(t *testing.T) { type testCase struct { name string fv *flagvar.Enum expected string } run := func(t *testing.T, tc testCase) { actual := tc.fv.String() assert.Equal(t, actual, tc.expected) } testCases := []testCase{ { name: "non-nil receiver", fv: &flagvar.Enum{Value: "yaml"}, expected: "yaml", }, { name: "nil receiver", fv: nil, expected: "", }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } func TestEnumSet(t *testing.T) { type testCase struct { name string input string expected string err error } run := func(t *testing.T, tc testCase) { fv := &flagvar.Enum{ Choices: []string{"yaml", "json"}, } err := fv.Set(tc.input) if err != nil { assert.ErrorIs(t, err, tc.err) } else { assert.Equal(t, fv.Value, tc.expected) } } testCases := []testCase{ { name: "valid enum option", input: "yaml", expected: "yaml", }, { name: "ignores option capitalization", input: "YaMl", expected: "yaml", }, { name: "invalid enum option", input: "cue", err: flagvar.ErrInvalidEnum, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } ================================================ FILE: pkg/flagvar/file.go ================================================ package flagvar import ( "os" ) // File is a flag.Value for file paths. Returns any errors from os.Stat. type File struct { Value string } // Help returns a string to include in the flag's help message. func (f *File) Help() string { return "file path" } // Set implements flag.Value by checking for the file's existence through // using os.Stat. Any error returned by os.Stat is returned by this function. func (f *File) Set(v string) error { _, err := os.Stat(v) f.Value = v return err } // String implements flag.Value by returning the current file path. func (f *File) String() string { if f == nil { return "" } return f.Value } // Type implements pflag.Value by noting our Value is string typed. func (f *File) Type() string { return "string" } ================================================ FILE: pkg/flagvar/file_test.go ================================================ package flagvar_test import ( "io/fs" "path/filepath" "testing" "github.com/cloudflare/lockbox/pkg/flagvar" "gotest.tools/v3/assert" ) func TestFileString(t *testing.T) { type testCase struct { name string fv *flagvar.File expected string } run := func(t *testing.T, tc testCase) { actual := tc.fv.String() assert.Equal(t, actual, tc.expected) } testCases := []testCase{ { name: "non-nil receiver", fv: &flagvar.File{Value: "/path/to/default.log"}, expected: "/path/to/default.log", }, { name: "nil receiver", fv: nil, expected: "", }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } func TestFileSet(t *testing.T) { type testCase struct { name string input string expected string err error } run := func(t *testing.T, tc testCase) { fv := &flagvar.File{} err := fv.Set(tc.input) if tc.err != nil { assert.ErrorIs(t, err, tc.err) } else { assert.Equal(t, fv.Value, tc.expected) } } testCases := []testCase{ { name: "file exists", input: filepath.Join("testdata", "file"), expected: "testdata/file", }, { name: "file does not exist", input: filepath.Join("testdata", "file_nonexistant.go"), err: fs.ErrNotExist, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } ================================================ FILE: pkg/flagvar/tcp_addr.go ================================================ package flagvar import "net" // TCPAddr is a flag.Value for file paths. Returns any errors from net.ResolveTCPAddr. type TCPAddr struct { Network string Value *net.TCPAddr Text string } // Help returns a string to include in the flag's help message. func (t *TCPAddr) Help() string { return "TCP address in host:port format" } // Set implements flag.Value by parsing the provided address using net.ResolveTCPAddr. // Any error return is returned by this function. func (t *TCPAddr) Set(v string) error { network := "tcp" if t.Network != "" { network = t.Network } tcpAddr, err := net.ResolveTCPAddr(network, v) t.Text = v t.Value = tcpAddr return err } // String implements flag.Value by returning the current Text. func (t *TCPAddr) String() string { if t == nil { return "" } return t.Text } // Type implements pflag.Value by noting our Value is net.TCPAddr typed. func (t *TCPAddr) Type() string { return "net.TCPAddr" } ================================================ FILE: pkg/flagvar/tcp_addr_test.go ================================================ package flagvar_test import ( "net" "testing" "github.com/cloudflare/lockbox/pkg/flagvar" "gotest.tools/v3/assert" ) func TestTCPAddrString(t *testing.T) { type testCase struct { name string fv *flagvar.TCPAddr expected string } run := func(t *testing.T, tc testCase) { actual := tc.fv.String() assert.Equal(t, actual, tc.expected) } testCases := []testCase{ { name: "non-nil receiver", fv: &flagvar.TCPAddr{Text: ":8080"}, expected: ":8080", }, { name: "nil receiver", fv: nil, expected: "", }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } func TestTCPAddrSet(t *testing.T) { type testCase struct { name string input string expected *net.TCPAddr err string } run := func(t *testing.T, tc testCase) { fv := flagvar.TCPAddr{} err := fv.Set(tc.input) if err != nil { assert.Error(t, err, tc.err) } else { assert.DeepEqual(t, fv.Value, tc.expected) } } testCases := []testCase{ { name: "host:port address", input: "127.0.0.1:8080", expected: &net.TCPAddr{ IP: net.ParseIP("127.0.0.1"), Port: 8080, }, }, { name: "port-only address", input: ":8080", expected: &net.TCPAddr{ Port: 8080, }, }, { name: "IPv6 support", input: "[::1]:8080", expected: &net.TCPAddr{ IP: net.ParseIP("::1"), Port: 8080, }, }, { name: "invalid address", input: "google.com", expected: nil, err: "address google.com: missing port in address", }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } ================================================ FILE: pkg/flagvar/testdata/file ================================================ ================================================ FILE: pkg/lockbox-controller/secretreconciler.go ================================================ package controller import ( "context" "encoding/base64" "fmt" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" "github.com/cloudflare/lockbox/pkg/util/conditions" "github.com/kevinburke/nacl" "github.com/kevinburke/nacl/box" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) //go:generate controller-gen rbac:roleName=lockbox-controller paths=./. output:rbac:artifacts:config=../../deployment/rbac // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;patch;update // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch // +kubebuilder:rbac:groups="lockbox.k8s.cloudflare.com",resources=lockboxes,verbs=get;list;watch // +kubebuilder:rbac:groups="lockbox.k8s.cloudflare.com",resources=lockboxes/status,verbs=get;update;patch const keySize = nacl.KeySize // SecretReconcilerOption allows for functional options to modify the SecretReconciler type SecretReconcilerOption func(s *SecretReconciler) // SecretReconciler implements the reconciliation logic for Lockbox secrets. type SecretReconciler struct { pubKey, priKey nacl.Key client client.Client recorder record.EventRecorder } // NewSecretReconciler creates a reconciler controller for the provided keypair and options. // // If not mutated by any options, the reconciler uses a noop API client and events recorder. func NewSecretReconciler(pubKey, priKey nacl.Key, options ...SecretReconcilerOption) *SecretReconciler { sr := &SecretReconciler{ pubKey: pubKey, priKey: priKey, client: clientfake.NewClientBuilder().Build(), recorder: &record.FakeRecorder{}, } for _, opt := range options { opt(sr) } return sr } // Reconcile implements reconcile.Reconciler by ensuring Lockbox controlled Secrets are as described. func (s *SecretReconciler) Reconcile(ctx context.Context, lb *lockboxv1.Lockbox) (reconcile.Result, error) { if len(lb.Spec.Sender) != keySize { msg := fmt.Sprintf("invalid sender key length, got %d wanted %d", len(lb.Spec.Sender), keySize) s.recorder.Eventf(lb, "Warning", "InvalidKeyLength", msg) conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidKeyLength", lockboxv1.ConditionSeverityError, msg)) _ = s.client.Status().Update(ctx, lb) return reconcile.Result{}, fmt.Errorf("incorrect sender key length: %d, should be %d", len(lb.Spec.Sender), keySize) } if len(lb.Spec.Peer) != keySize { msg := fmt.Sprintf("invalid peer key length, got %d wanted %d", len(lb.Spec.Peer), keySize) s.recorder.Eventf(lb, "Warning", "InvalidKeyLength", msg) conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidKeyLength", lockboxv1.ConditionSeverityError, msg)) _ = s.client.Status().Update(ctx, lb) return reconcile.Result{}, fmt.Errorf("incorrect peer key length: %d, should be %d", len(lb.Spec.Peer), keySize) } peerKey := new([keySize]byte) copy(peerKey[:], lb.Spec.Peer) if !nacl.Verify32(peerKey, s.pubKey) { msg := fmt.Sprintf("lockbox has unknown peer key %q", base64.StdEncoding.EncodeToString(lb.Spec.Peer)) s.recorder.Eventf(lb, "Warning", "UnknownPeerKey", msg) conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "UnknownPeerKey", lockboxv1.ConditionSeverityError, msg)) _ = s.client.Status().Update(ctx, lb) return reconcile.Result{}, fmt.Errorf("unknown peer key") } sender := new([keySize]byte) copy(sender[:], lb.Spec.Sender) secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: lb.Name, Namespace: lb.Namespace, }, } namespace, err := box.EasyOpen(lb.Spec.Namespace, sender, s.priKey) if err != nil { msg := fmt.Sprintf("unable to open lockbox with peer key %q", base64.StdEncoding.EncodeToString(lb.Spec.Peer)) s.recorder.Eventf(lb, "Warning", "InvalidLockbox", msg) conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidLockbox", lockboxv1.ConditionSeverityError, msg)) _ = s.client.Status().Update(ctx, lb) return reconcile.Result{}, err } if string(namespace) != lb.Namespace { msg := fmt.Sprintf("locked for namespace %q, found in namespace %s", namespace, lb.Namespace) s.recorder.Eventf(lb, "Warning", "InvalidNamespace", msg) conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidNamespace", lockboxv1.ConditionSeverityWarning, msg)) _ = s.client.Status().Update(ctx, lb) return reconcile.Result{}, fmt.Errorf("incorrect namespace: %s, should be %s", namespace, lb.Namespace) } _, err = controllerutil.CreateOrPatch( ctx, s.client, secret, s.reconcileExisting(lb, sender, secret)) if err != nil { conditions.Set(lb, conditions.FalseCondition(lockboxv1.ReadyCondition, "InvalidLockbox", lockboxv1.ConditionSeverityWarning, err.Error())) _ = s.client.Status().Update(ctx, lb) return reconcile.Result{}, err } conditions.Set(lb, conditions.TrueCondition(lockboxv1.ReadyCondition)) _ = s.client.Status().Update(ctx, lb) return reconcile.Result{}, nil } // reconcileExisting returns a function suitable for controllerutil.CreateOrUpdate that mutates a Secret object // to reflect the desired state. func (s *SecretReconciler) reconcileExisting(lb *lockboxv1.Lockbox, sender nacl.Key, secret *corev1.Secret) func() error { return func() error { if err := controllerutil.SetControllerReference(lb, secret, s.client.Scheme()); err != nil { switch err := err.(type) { case decryptSecretKeyErrorer: s.recorder.Eventf(lb, "Warning", "InvalidLockbox", "lockbox contained key %q that could not be unlocked", err.SecretKey()) default: s.recorder.Eventf(lb, "Warning", "InvalidLockbox", "lockbox could not be unlocked") } return err } return lb.UnlockInto(secret, s.priKey) } } // WithRecorder sets the EventRecorder used by the SecretReconciler. func WithRecorder(r record.EventRecorder) SecretReconcilerOption { return func(s *SecretReconciler) { s.recorder = r } } // WithClient sets the API Client used by the SecretReconciler func WithClient(c client.Client) SecretReconcilerOption { return func(s *SecretReconciler) { s.client = c } } // decryptSecretKeyErrorer matches the unexported error type, to // fetch the secret data key that triggered the error. type decryptSecretKeyErrorer interface { SecretKey() string } ================================================ FILE: pkg/lockbox-controller/secretreconciler_suite_test.go ================================================ //go:build suite // +build suite package controller_test import ( "context" "fmt" "log" "os" "path/filepath" "testing" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" . "github.com/cloudflare/lockbox/pkg/lockbox-controller" "github.com/go-logr/zerologr" "github.com/google/go-cmp/cmp/cmpopts" "github.com/rs/zerolog" "gotest.tools/v3/assert" "gotest.tools/v3/poll" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) var cfg *rest.Config func TestMain(m *testing.M) { zl := zerolog.New(os.Stderr) logf.SetLogger(zerologr.New(&zl)) t := &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "deployment", "crds")}, } lockboxv1.AddToScheme(scheme.Scheme) var err error if cfg, err = t.Start(); err != nil { log.Fatal(err) } code := m.Run() t.Stop() os.Exit(code) } func TestSuiteSecretReconciler(t *testing.T) { type testCase struct { name string lockboxName string resources []client.Object expected *corev1.Secret } pubKey, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") assert.NilError(t, err) setup := func(t *testing.T, tc testCase) { mgr, err := manager.New(cfg, manager.Options{ Metrics: metricsserver.Options{ BindAddress: "0", }, Scheme: scheme.Scheme, }) assert.NilError(t, err) sr := NewSecretReconciler(pubKey, priKey, WithClient(mgr.GetClient())) err = builder. ControllerManagedBy(mgr). For(&lockboxv1.Lockbox{}). Owns(&corev1.Secret{}). Complete(reconcile.AsReconciler(mgr.GetClient(), sr)) assert.NilError(t, err) ctx, cancel := context.WithCancel(context.Background()) go func() { mgr.Start(ctx) }() t.Cleanup(cancel) } run := func(t *testing.T, tc testCase) { c, err := client.New(cfg, client.Options{}) assert.NilError(t, err) for _, r := range tc.resources { c.Create(context.Background(), r) } secret := &corev1.Secret{} poll.WaitOn(t, func(t poll.LogT) poll.Result { err := c.Get(context.Background(), client.ObjectKey{ Name: "example", Namespace: "default", }, secret) if err == nil { return poll.Success() } if apierrors.IsNotFound(err) { return poll.Continue("secret was not found") } return poll.Error(err) }) cm := &corev1.ConfigMap{} c.Get(context.Background(), client.ObjectKey{ Name: "example", Namespace: "default", }, cm) fmt.Printf("cm: %+v\n", *cm) assert.DeepEqual(t, secret, tc.expected, cmpopts.IgnoreFields(metav1.ObjectMeta{}, "UID", "ResourceVersion", "CreationTimestamp", "ManagedFields"), cmpopts.IgnoreFields(metav1.OwnerReference{}, "UID"), ) for _, r := range tc.resources { c.Delete(context.Background(), r) } // delete the created resource too, as there's no garbage collector c.Delete(context.Background(), tc.expected) } testCases := []testCase{ { name: "create secret", lockboxName: "example", resources: []client.Object{ &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Labels: map[string]string{ "type": "lockbox", }, Annotations: map[string]string{ "helm.sh/hook": "pre-install", }, }, Spec: lockboxv1.LockboxSpec{ Sender: []byte{0x74, 0xbd, 0xd8, 0x82, 0xf7, 0xd5, 0x87, 0xde, 0x08, 0x79, 0xf0, 0x9b, 0x35, 0x15, 0xf5, 0x2d, 0x1f, 0xb0, 0x26, 0xb3, 0x20, 0xe1, 0xe1, 0xd8, 0x5c, 0x5a, 0x0e, 0x1d, 0xfb, 0x80, 0x87, 0x23}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x01, 0x1f, 0xb8, 0x8c, 0x01, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0x0d, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0x3a, 0x1a, 0x82, 0xd1, 0xad, 0x9f, 0x89, 0x6b, 0x59, 0x8e, 0xce, 0x45, 0xbc, 0x6f, 0x61, 0x34, 0x81, 0x7b, 0x7e, 0x2f, 0xa4, 0xd7, 0x15, 0xaf, 0x28, 0x15, 0xc0, 0x3e, 0x21, 0xfc, 0xcb, 0x3a, 0x38, 0x60, 0x96, 0xc7, 0xac, 0xe6, 0x56, 0xf2, 0xb7, 0x40, 0x4e, 0x9e, 0xb4, 0xbf, 0x96}, Template: lockboxv1.LockboxSecretTemplate{ LockboxSecretTemplateMetadata: lockboxv1.LockboxSecretTemplateMetadata{ Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, }, }, Data: map[string][]byte{ "test": {0x57, 0x17, 0x83, 0x22, 0x4c, 0x54, 0x1a, 0xb8, 0x83, 0x86, 0xc6, 0x15, 0xed, 0x23, 0x10, 0x58, 0x1d, 0xbc, 0x20, 0x47, 0xb4, 0x2a, 0x7f, 0xf6, 0xda, 0x4e, 0xa4, 0x88, 0x6b, 0x54, 0xed, 0xf6, 0xa3, 0x21, 0x73, 0xda, 0xca, 0x2b, 0xf7, 0x88, 0x13, 0xaa, 0xc2, 0xef}, }, }, }, }, expected: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, OwnerReferences: []metav1.OwnerReference{ { APIVersion: "lockbox.k8s.cloudflare.com/v1", Kind: "Lockbox", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ "test": []byte("test"), }, }, }, { name: "avoids updating secrets owned by other controllers", lockboxName: "example", resources: []client.Object{ &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "ConfigMap", Name: "example", UID: "deadbeef", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Data: map[string][]byte{ "test": []byte("test"), "test1": []byte("test1"), }, }, &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", }, Spec: lockboxv1.LockboxSpec{ Sender: []byte{0x74, 0xbd, 0xd8, 0x82, 0xf7, 0xd5, 0x87, 0xde, 0x08, 0x79, 0xf0, 0x9b, 0x35, 0x15, 0xf5, 0x2d, 0x1f, 0xb0, 0x26, 0xb3, 0x20, 0xe1, 0xe1, 0xd8, 0x5c, 0x5a, 0x0e, 0x1d, 0xfb, 0x80, 0x87, 0x23}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x01, 0x1f, 0xb8, 0x8c, 0x01, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0x0d, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0x3a, 0x1a, 0x82, 0xd1, 0xad, 0x9f, 0x89, 0x6b, 0x59, 0x8e, 0xce, 0x45, 0xbc, 0x6f, 0x61, 0x34, 0x81, 0x7b, 0x7e, 0x2f, 0xa4, 0xd7, 0x15, 0xaf, 0x28, 0x15, 0xc0, 0x3e, 0x21, 0xfc, 0xcb, 0x3a, 0x38, 0x60, 0x96, 0xc7, 0xac, 0xe6, 0x56, 0xf2, 0xb7, 0x40, 0x4e, 0x9e, 0xb4, 0xbf, 0x96}, Data: map[string][]byte{ "test": {0x57, 0x17, 0x83, 0x22, 0x4c, 0x54, 0x1a, 0xb8, 0x83, 0x86, 0xc6, 0x15, 0xed, 0x23, 0x10, 0x58, 0x1d, 0xbc, 0x20, 0x47, 0xb4, 0x2a, 0x7f, 0xf6, 0xda, 0x4e, 0xa4, 0x88, 0x6b, 0x54, 0xed, 0xf6, 0xa3, 0x21, 0x73, 0xda, 0xca, 0x2b, 0xf7, 0x88, 0x13, 0xaa, 0xc2, 0xef}, }, }, }, }, expected: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "default", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "v1", Kind: "ConfigMap", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ "test": []byte("test"), "test1": []byte("test1"), }, }, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { setup(t, tc) run(t, tc) }) } } ================================================ FILE: pkg/lockbox-controller/secretreconciler_test.go ================================================ package controller_test import ( "context" "testing" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" controller "github.com/cloudflare/lockbox/pkg/lockbox-controller" "github.com/kevinburke/nacl" "gotest.tools/v3/assert" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) func TestSecretReconciler(t *testing.T) { type testCase struct { name string lockboxName string resources []client.Object expected *corev1.Secret expectedErr string } run := func(t *testing.T, tc testCase) { scheme := runtime.NewScheme() assert.NilError(t, corev1.AddToScheme(scheme)) assert.NilError(t, lockboxv1.AddToScheme(scheme)) client := clientfake.NewClientBuilder(). WithObjects(tc.resources...). WithScheme(scheme). Build() pubKey, priKey, err := loadKeypair(t, "6a42b9fc2b011fb88c01741483e3bffe455bdab1ae35d0bb53a3c00d406d8836", "252173f975f0a0ddb198a7e5958c074203a0e9f44275e0b840f95d456c4acc2e") assert.NilError(t, err) lsn := types.NamespacedName{Name: tc.lockboxName, Namespace: "example"} sr := controller.NewSecretReconciler(pubKey, priKey, controller.WithClient(client)) _, err = reconcile.AsReconciler(client, sr).Reconcile(context.Background(), reconcile.Request{NamespacedName: lsn}) if tc.expectedErr != "" { assert.ErrorContains(t, err, tc.expectedErr) } else { assert.NilError(t, err) } actual := &corev1.Secret{} err = client.Get(context.Background(), lsn, actual) if tc.expected == nil { assert.Assert(t, apierrors.IsNotFound(err)) return } assert.NilError(t, err) assert.DeepEqual(t, actual, tc.expected) } testCases := []testCase{ { name: "new lockbox", lockboxName: "example", resources: []client.Object{ &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", Labels: map[string]string{ "type": "lockbox", }, Annotations: map[string]string{ "helm.sh/hook": "pre-install", }, }, Spec: lockboxv1.LockboxSpec{ Sender: []byte{0xb2, 0xa3, 0xf, 0x85, 0xa, 0x58, 0xcf, 0x94, 0x4c, 0x62, 0x37, 0xd4, 0xef, 0xf5, 0xed, 0x11, 0x52, 0xfa, 0x1b, 0xc3, 0xb0, 0x4d, 0x27, 0xd5, 0x58, 0x67, 0x61, 0x67, 0xe0, 0x10, 0xb1, 0x5c}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0x4d, 0xa0, 0x73, 0x8b, 0x95, 0xc3, 0xd4, 0x64, 0xe9, 0xab, 0xd, 0xb7, 0x1e, 0x5, 0x10, 0xed, 0x4c, 0x2f, 0x8a, 0x66, 0x6d, 0xec, 0x7c, 0x5d, 0x9b, 0xa7, 0xb7, 0x88, 0x49, 0x8a, 0xb9, 0x7f, 0xf0, 0x30, 0xe0, 0xad, 0x49, 0x7c, 0x3f, 0xe3, 0x1c, 0x2e, 0xe9, 0xb1, 0x2a, 0x70, 0x28}, Template: lockboxv1.LockboxSecretTemplate{ LockboxSecretTemplateMetadata: lockboxv1.LockboxSecretTemplateMetadata{ Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, }, Type: corev1.SecretTypeOpaque, }, Data: map[string][]byte{ "test": {0x7b, 0xca, 0x32, 0x90, 0xf7, 0x97, 0x3b, 0x6, 0xfb, 0x7c, 0xdc, 0x3a, 0x25, 0x82, 0x29, 0xdf, 0x9d, 0x1e, 0x46, 0x8d, 0xd4, 0x99, 0x49, 0x2, 0x63, 0x56, 0x54, 0x64, 0xae, 0x9e, 0xf2, 0xc0, 0x35, 0xf5, 0xf1, 0xcb, 0x67, 0xb7, 0xe2, 0xb1, 0x14, 0x42, 0x71, 0xc}, "test1": {0x2c, 0x68, 0xed, 0x53, 0x55, 0x55, 0xe2, 0x2d, 0x71, 0x96, 0x85, 0xfd, 0xdb, 0x93, 0x1e, 0x77, 0x91, 0x2d, 0x76, 0xba, 0xae, 0x46, 0x30, 0x9e, 0xb6, 0x65, 0xa2, 0x49, 0xfe, 0x78, 0xc0, 0xcb, 0x6d, 0xf, 0xa8, 0xeb, 0xa8, 0xfc, 0xc0, 0xa0, 0xdc, 0x4, 0x16, 0x7, 0xa0}, }, }, }, }, expected: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, ResourceVersion: "1", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "lockbox.k8s.cloudflare.com/v1", Kind: "Lockbox", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ "test": []byte("test"), "test1": []byte("test1"), }, }, }, { name: "update lockbox secret", lockboxName: "example", resources: []client.Object{ &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", }, Spec: lockboxv1.LockboxSpec{ Sender: []byte{0xa, 0xda, 0x33, 0xf3, 0x48, 0xad, 0xb6, 0x4c, 0xaa, 0x6, 0x50, 0xc1, 0xe1, 0xa6, 0xeb, 0x49, 0x13, 0xe0, 0x53, 0xdf, 0xde, 0x44, 0x72, 0xd6, 0xe2, 0x51, 0x94, 0xee, 0xcb, 0xba, 0xc1, 0x4}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0xa7, 0x4c, 0x72, 0x7a, 0x71, 0x1d, 0x98, 0x32, 0xa, 0x3, 0xbe, 0xe5, 0x9d, 0xd4, 0x8c, 0x39, 0x3, 0x42, 0x9c, 0x5e, 0xeb, 0x6d, 0x95, 0x46, 0x5c, 0x10, 0x62, 0xa3, 0xa7, 0xfb, 0xee, 0x19, 0xcb, 0x98, 0xbf, 0xc1, 0x19, 0x66, 0x6a, 0x77, 0x76, 0x22, 0x17, 0x8f, 0xa5, 0x24, 0x8e}, Data: map[string][]byte{ "updated": {0x78, 0x70, 0x68, 0xae, 0x9f, 0xf5, 0xed, 0x60, 0x74, 0x14, 0x6a, 0xc5, 0xc3, 0xb, 0xe2, 0xaa, 0x20, 0x68, 0x7a, 0xfb, 0xa6, 0x6a, 0x38, 0xc2, 0x20, 0x73, 0xb5, 0x45, 0x9f, 0x9, 0xf0, 0x15, 0xd1, 0x5c, 0x16, 0x51, 0x50, 0xaa, 0xea, 0x68, 0x3a, 0x95, 0xe6}, }, Template: lockboxv1.LockboxSecretTemplate{ Type: corev1.SecretTypeOpaque, }, }, }, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", Labels: map[string]string{ "type": "secret", }, Annotations: map[string]string{ "wave": "ignore", }, ResourceVersion: "1", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "lockbox.k8s.cloudflare.com/v1", Kind: "Lockbox", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ "test": []byte("test"), "test1": []byte("test1"), }, }, }, expected: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", ResourceVersion: "2", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "lockbox.k8s.cloudflare.com/v1", Kind: "Lockbox", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ "updated": []byte("yep"), }, }, }, { name: "secret conflict", lockboxName: "example", resources: []client.Object{ &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", }, Spec: lockboxv1.LockboxSpec{ Sender: []byte{0xa, 0xda, 0x33, 0xf3, 0x48, 0xad, 0xb6, 0x4c, 0xaa, 0x6, 0x50, 0xc1, 0xe1, 0xa6, 0xeb, 0x49, 0x13, 0xe0, 0x53, 0xdf, 0xde, 0x44, 0x72, 0xd6, 0xe2, 0x51, 0x94, 0xee, 0xcb, 0xba, 0xc1, 0x4}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0xa7, 0x4c, 0x72, 0x7a, 0x71, 0x1d, 0x98, 0x32, 0xa, 0x3, 0xbe, 0xe5, 0x9d, 0xd4, 0x8c, 0x39, 0x3, 0x42, 0x9c, 0x5e, 0xeb, 0x6d, 0x95, 0x46, 0x5c, 0x10, 0x62, 0xa3, 0xa7, 0xfb, 0xee, 0x19, 0xcb, 0x98, 0xbf, 0xc1, 0x19, 0x66, 0x6a, 0x77, 0x76, 0x22, 0x17, 0x8f, 0xa5, 0x24, 0x8e}, Data: map[string][]byte{ "updated": {0x78, 0x70, 0x68, 0xae, 0x9f, 0xf5, 0xed, 0x60, 0x74, 0x14, 0x6a, 0xc5, 0xc3, 0xb, 0xe2, 0xaa, 0x20, 0x68, 0x7a, 0xfb, 0xa6, 0x6a, 0x38, 0xc2, 0x20, 0x73, 0xb5, 0x45, 0x9f, 0x9, 0xf0, 0x15, 0xd1, 0x5c, 0x16, 0x51, 0x50, 0xaa, 0xea, 0x68, 0x3a, 0x95, 0xe6}, }, }, }, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", ResourceVersion: "1", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "bitnami.com/v1alpha1", Kind: "SealedSecret", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ "test": []byte("test"), "test1": []byte("test1"), }, }, }, expected: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", ResourceVersion: "1", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "bitnami.com/v1alpha1", Kind: "SealedSecret", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeOpaque, Data: map[string][]byte{ "test": []byte("test"), "test1": []byte("test1"), }, }, expectedErr: "already owned by another SealedSecret controller example", }, { name: "docker-registry secret", lockboxName: "example", resources: []client.Object{ &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", }, Spec: lockboxv1.LockboxSpec{ Sender: []byte{0x64, 0x22, 0x82, 0xe9, 0x35, 0x2a, 0x36, 0x7a, 0x40, 0x75, 0xd5, 0x14, 0xa6, 0x24, 0xef, 0xe3, 0x59, 0xda, 0xf5, 0xe9, 0xbe, 0xc8, 0x2d, 0x88, 0xb1, 0x17, 0xe8, 0xf2, 0x99, 0xb0, 0x9f, 0x71}, Peer: []byte{0x6a, 0x42, 0xb9, 0xfc, 0x2b, 0x1, 0x1f, 0xb8, 0x8c, 0x1, 0x74, 0x14, 0x83, 0xe3, 0xbf, 0xfe, 0x45, 0x5b, 0xda, 0xb1, 0xae, 0x35, 0xd0, 0xbb, 0x53, 0xa3, 0xc0, 0xd, 0x40, 0x6d, 0x88, 0x36}, Namespace: []byte{0xd3, 0x1c, 0xc6, 0x29, 0x65, 0xac, 0xd6, 0x5, 0x3a, 0x60, 0xe1, 0x7c, 0xf8, 0xb9, 0x7, 0xdd, 0xdb, 0xf0, 0x82, 0xab, 0x90, 0x38, 0x7, 0x56, 0x72, 0x68, 0xef, 0x56, 0x3b, 0xae, 0x13, 0x16, 0x7e, 0x3e, 0xf6, 0xaf, 0xb4, 0x7b, 0x10, 0xed, 0x77, 0x29, 0xae, 0xcb, 0x96, 0x7f, 0xc9}, Data: map[string][]byte{ ".dockerconfigjson": {0x98, 0x39, 0x7b, 0x93, 0x7a, 0xb6, 0x4, 0xc, 0xb8, 0x52, 0xf0, 0x97, 0x2e, 0x74, 0xed, 0xd6, 0x41, 0x7a, 0x7d, 0x20, 0xda, 0x35, 0x2d, 0xdf, 0x2b, 0x94, 0x9f, 0x78, 0x78, 0xd4, 0x29, 0x30, 0x6d, 0xbf, 0x9c, 0x59, 0x9f, 0xb4, 0x47, 0x5e, 0x10, 0x4a, 0xd2, 0xf, 0xd8, 0x77, 0x7d, 0x8, 0x11, 0x36, 0x41, 0xa9, 0xb2, 0x77, 0xac, 0xd9, 0xa3, 0x8, 0x81, 0x0, 0x6, 0x34, 0xde, 0x3e, 0xfc, 0x38, 0x4c, 0xa4, 0x27, 0xff, 0x1f, 0x67, 0x8, 0xef, 0x6, 0xff, 0x31, 0x80, 0xd, 0x4e, 0xcf, 0x6c, 0xec, 0x79, 0x78, 0x7d, 0x9f, 0x5b, 0x34, 0xe4, 0x5a, 0x44, 0x49, 0x57, 0xfd, 0xeb, 0x43, 0xd4, 0x4e, 0xe5, 0x15, 0xbc, 0xa8, 0x5a, 0x86, 0xd, 0xb9, 0xaa, 0x45, 0x6c, 0x4b, 0x17, 0x66, 0x13, 0xb2, 0x8c, 0x46, 0x7e, 0xdc, 0xe, 0x21, 0x54, 0x39, 0x27, 0xa3, 0x93, 0x52, 0x46, 0xa1, 0x71, 0x21, 0x8e, 0x27, 0x62, 0x6b, 0x86, 0xa6, 0xe4, 0x98, 0xc4, 0xff, 0x8, 0xed, 0xba, 0x4d, 0xa1, 0xfa, 0x53, 0x25, 0xb7, 0x29, 0x20, 0x1a, 0xab, 0x4a, 0xf5, 0x99, 0x99, 0x6a, 0x9d, 0xb8, 0x96, 0x28, 0x9b, 0x6a, 0xda, 0xb8, 0xee, 0x9c, 0x5f, 0xc1, 0x91, 0x0, 0x38, 0x84, 0x90, 0xdf, 0xbd, 0x9a, 0x1b, 0x9e, 0xd6, 0xe4, 0x3d}, }, Template: lockboxv1.LockboxSecretTemplate{ Type: corev1.SecretTypeDockerConfigJson, }, }, }, }, expected: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "example", Namespace: "example", ResourceVersion: "1", OwnerReferences: []metav1.OwnerReference{ { APIVersion: "lockbox.k8s.cloudflare.com/v1", Kind: "Lockbox", Name: "example", Controller: ptr.To(true), BlockOwnerDeletion: ptr.To(true), }, }, }, Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ ".dockerconfigjson": {0x65, 0x79, 0x4a, 0x68, 0x64, 0x58, 0x52, 0x6f, 0x63, 0x79, 0x49, 0x36, 0x65, 0x79, 0x4a, 0x6b, 0x62, 0x32, 0x4e, 0x72, 0x5a, 0x58, 0x49, 0x75, 0x5a, 0x58, 0x68, 0x68, 0x62, 0x58, 0x42, 0x73, 0x5a, 0x53, 0x35, 0x6a, 0x62, 0x32, 0x30, 0x69, 0x4f, 0x6e, 0x73, 0x69, 0x56, 0x58, 0x4e, 0x6c, 0x63, 0x6d, 0x35, 0x68, 0x62, 0x57, 0x55, 0x69, 0x4f, 0x69, 0x4a, 0x71, 0x62, 0x32, 0x56, 0x6b, 0x5a, 0x58, 0x5a, 0x6c, 0x62, 0x47, 0x39, 0x77, 0x5a, 0x58, 0x49, 0x69, 0x4c, 0x43, 0x4a, 0x51, 0x59, 0x58, 0x4e, 0x7a, 0x64, 0x32, 0x39, 0x79, 0x5a, 0x43, 0x49, 0x36, 0x49, 0x6e, 0x42, 0x68, 0x63, 0x33, 0x4e, 0x33, 0x62, 0x33, 0x4a, 0x6b, 0x49, 0x69, 0x77, 0x69, 0x52, 0x57, 0x31, 0x68, 0x61, 0x57, 0x77, 0x69, 0x4f, 0x69, 0x4a, 0x71, 0x62, 0x32, 0x56, 0x41, 0x5a, 0x58, 0x68, 0x68, 0x62, 0x58, 0x42, 0x73, 0x5a, 0x53, 0x35, 0x6a, 0x62, 0x32, 0x30, 0x69, 0x66, 0x58, 0x31, 0x39}, }, }, }, } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { run(t, tc) }) } } func loadKeypair(t *testing.T, pub, pri string) (pubKey, priKey nacl.Key, err error) { t.Helper() pubKey, err = nacl.Load(pub) if err != nil { return } priKey, err = nacl.Load(pri) return } ================================================ FILE: pkg/lockbox-server/serve.go ================================================ package server import ( "net/http" "github.com/kevinburke/nacl" ) // PublicKey creates an HTTP handler that responses with the specified public key // as binary data. func PublicKey(pubKey nacl.Key) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/octet-stream") _, _ = w.Write(pubKey[:]) }) } ================================================ FILE: pkg/statemetrics/collector.go ================================================ package statemetrics import ( "sync" "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/types" ) // Kubernetes is a metric tracking a numerical value that can arbitrary go up and down. type Kubernetes interface { prometheus.Metric prometheus.Collector // Set sets the tracked number to an arbitrary value. Set(float64) } // kubernetes implements Kubernetes. type kubernetes struct { prometheus.Metric values []string desc *prometheus.Desc } // Set creates a new constant metric for this numerical value func (k *kubernetes) Set(val float64) { k.Metric = prometheus.MustNewConstMetric(k.desc, prometheus.GaugeValue, val, k.values...) } // Describe implements Collector func (k *kubernetes) Describe(ch chan<- *prometheus.Desc) { ch <- k.desc } // Collect implements Collector func (k *kubernetes) Collect(ch chan<- prometheus.Metric) { ch <- k.Metric } // KubernetesVec is a Collector that bundles a set of Kubernetes metrics that all share the same Desc, // but have different values for their variable labels. This is used if you want to count the same // thing partitioned by various dimensions (e.g., namespaces, types). Create instances with // NewKubernetesVec. type KubernetesVec struct { desc *prometheus.Desc metrics map[types.UID]Kubernetes mu sync.Mutex } // KubernetesOpts is an alias for Opts. type KubernetesOpts prometheus.Opts // NewKubernetesVec creates a new KubernetesVec based on the provided KubernetesOpts and partitioned // by the given label names. func NewKubernetesVec(opts KubernetesOpts, labelNames []string) *KubernetesVec { desc := prometheus.NewDesc( prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name), opts.Help, labelNames, opts.ConstLabels, ) return &KubernetesVec{ desc: desc, metrics: make(map[types.UID]Kubernetes), } } // WithLabelValues returns the Kubernetes metric for the given slice of label values (in the same order // as the variable labels). // // Consecutive calls for the same uid replace earlier metrics. func (v *KubernetesVec) WithLabelValues(uid types.UID, lvs ...string) Kubernetes { k := &kubernetes{ values: lvs, desc: v.desc, } k.Set(0) v.mu.Lock() v.metrics[uid] = k v.mu.Unlock() return k } // Delete deletes the metric stored for this uid. func (v *KubernetesVec) Delete(uid types.UID) { v.mu.Lock() defer v.mu.Unlock() delete(v.metrics, uid) } // Describe implements Collector. func (v *KubernetesVec) Describe(ch chan<- *prometheus.Desc) { ch <- v.desc } // Collect implements Collector. func (v *KubernetesVec) Collect(ch chan<- prometheus.Metric) { v.mu.Lock() defer v.mu.Unlock() for _, metric := range v.metrics { ch <- metric } } // LabelsVec is a Collector that bundles a set of unchecked Kubernetes metrics that all share the // same Desc, but have different labels. This is used if you want to count the same thing // partitioned by unbounded dimensions (e.g., Kubernetes labels). Create instances with // NewLabelsVec. type LabelsVec struct { opts KubernetesOpts metrics map[types.UID]Kubernetes mu sync.Mutex } // NewLabelsVec creates a new LabelsVec based on the provided KubernetesOpts. func NewLabelsVec(opts KubernetesOpts) *LabelsVec { return &LabelsVec{ opts: opts, metrics: make(map[types.UID]Kubernetes), } } // With returns the Kubernetes metric for the given Labels map. // // Consecutive calls with the same uid replace earlier metrics. func (v *LabelsVec) With(uid types.UID, l prometheus.Labels) Kubernetes { labels := make([]string, 0, len(l)) values := make([]string, 0, len(l)) for label, value := range l { labels = append(labels, label) values = append(values, value) } desc := prometheus.NewDesc( prometheus.BuildFQName(v.opts.Namespace, v.opts.Subsystem, v.opts.Name), v.opts.Help, labels, v.opts.ConstLabels, ) k := &kubernetes{ values: values, desc: desc, } k.Set(0) v.mu.Lock() v.metrics[uid] = k v.mu.Unlock() return k } // Delete deletes the metric stored for this uid. func (v *LabelsVec) Delete(uid types.UID) { v.mu.Lock() defer v.mu.Unlock() delete(v.metrics, uid) } // Describe implements Collector. No Desc are sent on the channel to make this // an unchecked collector. func (v *LabelsVec) Describe(chan<- *prometheus.Desc) {} // Collect implements Collector. func (v *LabelsVec) Collect(ch chan<- prometheus.Metric) { v.mu.Lock() defer v.mu.Unlock() for _, metric := range v.metrics { ch <- metric } } ================================================ FILE: pkg/statemetrics/handler.go ================================================ package statemetrics import ( "context" "encoding/hex" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" "k8s.io/client-go/util/workqueue" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" ) // StateMetricProxy updates state metrics by intercepting events as they are passed to event handlers. type StateMetricProxy struct { enqueuer handler.EventHandler info *KubernetesVec created *KubernetesVec resourceVersion *KubernetesVec lbType *KubernetesVec peerKey *KubernetesVec labels *LabelsVec } // NewStateMetricProxy returns a StateMetricsProxy. All metrics must be non-nil. func NewStateMetricProxy(enqueuer handler.EventHandler, info, created, resourceVersion, lbType, peerKey *KubernetesVec, labels *LabelsVec) *StateMetricProxy { return &StateMetricProxy{ enqueuer: enqueuer, info: info, created: created, resourceVersion: resourceVersion, lbType: lbType, peerKey: peerKey, labels: labels, } } // Create implements EventHandler. func (s *StateMetricProxy) Create(ctx context.Context, evt event.CreateEvent, q workqueue.RateLimitingInterface) { s.updateWith(evt.Object) if s.enqueuer != nil { s.enqueuer.Create(ctx, evt, q) } } // Update implements EventHandler. func (s *StateMetricProxy) Update(ctx context.Context, evt event.UpdateEvent, q workqueue.RateLimitingInterface) { s.updateWith(evt.ObjectNew) if s.enqueuer != nil { s.enqueuer.Update(ctx, evt, q) } } // Delete implements EventHandler. func (s *StateMetricProxy) Delete(ctx context.Context, evt event.DeleteEvent, q workqueue.RateLimitingInterface) { uid := evt.Object.GetUID() s.info.Delete(uid) s.created.Delete(uid) s.resourceVersion.Delete(uid) s.lbType.Delete(uid) s.peerKey.Delete(uid) s.labels.Delete(uid) if s.enqueuer != nil { s.enqueuer.Delete(ctx, evt, q) } } // Generic implements EventHandler. func (s *StateMetricProxy) Generic(ctx context.Context, evt event.GenericEvent, q workqueue.RateLimitingInterface) { if s.enqueuer != nil { s.enqueuer.Generic(ctx, evt, q) } } // updateWith updates the metrics for Create and Update handles. func (s *StateMetricProxy) updateWith(obj client.Object) { namespace := obj.GetNamespace() lockbox := obj.GetName() uid := obj.GetUID() s.info.WithLabelValues(uid, namespace, lockbox).Set(1) creationTime := obj.GetCreationTimestamp() if !creationTime.IsZero() { s.created.WithLabelValues(uid, namespace, lockbox).Set(float64(creationTime.Unix())) } s.resourceVersion.WithLabelValues(uid, namespace, lockbox, obj.GetResourceVersion()).Set(1) if lb, ok := obj.(*lockboxv1.Lockbox); ok { s.lbType.WithLabelValues(uid, namespace, lockbox, string(lb.Spec.Template.Type)).Set(1) s.peerKey.WithLabelValues(uid, namespace, lockbox, hex.EncodeToString(lb.Spec.Peer)).Set(1) } promLabels := kubernetesLabelsToPrometheusLabels(obj.GetLabels()) promLabels["namespace"] = namespace promLabels["lockbox"] = lockbox s.labels.With(uid, promLabels).Set(1) } ================================================ FILE: pkg/statemetrics/handler_test.go ================================================ package statemetrics import ( "context" "strings" "testing" "time" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/event" ) func TestStateMetricsProxy_Create(t *testing.T) { lb := &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fizz", Name: "buzz", UID: "foobar", ResourceVersion: "9001", CreationTimestamp: metav1.Date(2001, time.September, 9, 1, 46, 40, 0, time.UTC), Labels: map[string]string{ "testing": "true", }, }, Spec: lockboxv1.LockboxSpec{ Peer: []byte{0xDE, 0xAD, 0xBE, 0xEF}, Template: lockboxv1.LockboxSecretTemplate{ Type: "golang.org/testing", }, }, } info, created, resourceVersion, lbType, peerKey, labels := createMetricVectors(t) reg := prometheus.NewPedanticRegistry() reg.MustRegister(info, created, resourceVersion, lbType, peerKey, labels) evt := event.CreateEvent{Object: lb} handler := NewStateMetricProxy(nil, info, created, resourceVersion, lbType, peerKey, labels) handler.Create(context.Background(), evt, nil) expected := strings.NewReader(` # HELP kube_lockbox_info Information about Lockbox # TYPE kube_lockbox_info gauge kube_lockbox_info{lockbox="buzz",namespace="fizz"} 1 # HELP kube_lockbox_created Unix creation timestamp # TYPE kube_lockbox_created gauge kube_lockbox_created{lockbox="buzz",namespace="fizz"} 1e9 # HELP kube_lockbox_resource_version Resource version representing a specific version of a Lockbox # TYPE kube_lockbox_resource_version gauge kube_lockbox_resource_version{lockbox="buzz",namespace="fizz",resource_version="9001"} 1 # HELP kube_lockbox_type Lockbox secret type # TYPE kube_lockbox_type gauge kube_lockbox_type{lockbox="buzz",namespace="fizz",type="golang.org/testing"} 1 # HELP kube_lockbox_peer Lockbox peer key # TYPE kube_lockbox_peer gauge kube_lockbox_peer{lockbox="buzz",namespace="fizz",peer="deadbeef"} 1 # HELP kube_lockbox_labels Kubernetes labels converted to Prometheus labels # TYPE kube_lockbox_labels gauge kube_lockbox_labels{label_testing="true",lockbox="buzz",namespace="fizz"} 1 `) if err := testutil.GatherAndCompare(reg, expected); err != nil { t.Error(err) } } func TestStateMetricsProxy_Update(t *testing.T) { old := &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fizz", Name: "buzz", UID: "foobar", ResourceVersion: "8999", CreationTimestamp: metav1.Now(), Labels: map[string]string{ "testing": "false", }, }, Spec: lockboxv1.LockboxSpec{ Peer: []byte{0x00}, Template: lockboxv1.LockboxSecretTemplate{ Type: "example.org/old", }, }, } lb := &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fizz", Name: "buzz", UID: "foobar", ResourceVersion: "9001", CreationTimestamp: metav1.Date(2001, time.September, 9, 1, 46, 40, 0, time.UTC), Labels: map[string]string{ "testing": "true", }, }, Spec: lockboxv1.LockboxSpec{ Peer: []byte{0xDE, 0xAD, 0xBE, 0xEF}, Template: lockboxv1.LockboxSecretTemplate{ Type: "golang.org/testing", }, }, } info, created, resourceVersion, lbType, peerKey, labels := createMetricVectors(t) reg := prometheus.NewPedanticRegistry() reg.MustRegister(info, created, resourceVersion, lbType, peerKey, labels) create := event.CreateEvent{Object: old} upd := event.UpdateEvent{ ObjectOld: old, ObjectNew: lb, } handler := NewStateMetricProxy(nil, info, created, resourceVersion, lbType, peerKey, labels) handler.Create(context.Background(), create, nil) handler.Update(context.Background(), upd, nil) expected := strings.NewReader(` # HELP kube_lockbox_info Information about Lockbox # TYPE kube_lockbox_info gauge kube_lockbox_info{lockbox="buzz",namespace="fizz"} 1 # HELP kube_lockbox_created Unix creation timestamp # TYPE kube_lockbox_created gauge kube_lockbox_created{lockbox="buzz",namespace="fizz"} 1e9 # HELP kube_lockbox_resource_version Resource version representing a specific version of a Lockbox # TYPE kube_lockbox_resource_version gauge kube_lockbox_resource_version{lockbox="buzz",namespace="fizz",resource_version="9001"} 1 # HELP kube_lockbox_type Lockbox secret type # TYPE kube_lockbox_type gauge kube_lockbox_type{lockbox="buzz",namespace="fizz",type="golang.org/testing"} 1 # HELP kube_lockbox_peer Lockbox peer key # TYPE kube_lockbox_peer gauge kube_lockbox_peer{lockbox="buzz",namespace="fizz",peer="deadbeef"} 1 # HELP kube_lockbox_labels Kubernetes labels converted to Prometheus labels # TYPE kube_lockbox_labels gauge kube_lockbox_labels{label_testing="true",lockbox="buzz",namespace="fizz"} 1 `) if err := testutil.GatherAndCompare(reg, expected); err != nil { t.Error(err) } } func TestStateMetricsProxy_Delete(t *testing.T) { lb := &lockboxv1.Lockbox{ ObjectMeta: metav1.ObjectMeta{ Namespace: "fizz", Name: "buzz", UID: "foobar", ResourceVersion: "9001", CreationTimestamp: metav1.Date(2001, time.September, 9, 1, 46, 40, 0, time.UTC), Labels: map[string]string{ "testing": "true", }, }, Spec: lockboxv1.LockboxSpec{ Peer: []byte{0xDE, 0xAD, 0xBE, 0xEF}, Template: lockboxv1.LockboxSecretTemplate{ Type: "golang.org/testing", }, }, } info, created, resourceVersion, lbType, peerKey, labels := createMetricVectors(t) reg := prometheus.NewPedanticRegistry() reg.MustRegister(info, created, resourceVersion, lbType, peerKey, labels) create := event.CreateEvent{Object: lb} deleted := event.DeleteEvent{ Object: lb, DeleteStateUnknown: false, } handler := NewStateMetricProxy(nil, info, created, resourceVersion, lbType, peerKey, labels) handler.Create(context.Background(), create, nil) handler.Delete(context.Background(), deleted, nil) expected := &strings.Reader{} if err := testutil.GatherAndCompare(reg, expected); err != nil { t.Error(err) } } func createMetricVectors(t *testing.T) (info, created, resourceVersion, lbType, peerKey *KubernetesVec, labels *LabelsVec) { info = NewKubernetesVec(KubernetesOpts{ Name: "kube_lockbox_info", Help: "Information about Lockbox", }, []string{"namespace", "lockbox"}) created = NewKubernetesVec(KubernetesOpts{ Name: "kube_lockbox_created", Help: "Unix creation timestamp", }, []string{"namespace", "lockbox"}) resourceVersion = NewKubernetesVec(KubernetesOpts{ Name: "kube_lockbox_resource_version", Help: "Resource version representing a specific version of a Lockbox", }, []string{"namespace", "lockbox", "resource_version"}) lbType = NewKubernetesVec(KubernetesOpts{ Name: "kube_lockbox_type", Help: "Lockbox secret type", }, []string{"namespace", "lockbox", "type"}) peerKey = NewKubernetesVec(KubernetesOpts{ Name: "kube_lockbox_peer", Help: "Lockbox peer key", }, []string{"namespace", "lockbox", "peer"}) labels = NewLabelsVec(KubernetesOpts{ Name: "kube_lockbox_labels", Help: "Kubernetes labels converted to Prometheus labels", }) return } ================================================ FILE: pkg/statemetrics/labels.go ================================================ package statemetrics import ( "regexp" "github.com/prometheus/client_golang/prometheus" ) var invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`) // sanitizeLabel replaces non-alphanumeric characters with underscores. func sanitizeLabel(l string) string { return invalidLabelCharRE.ReplaceAllString(l, "_") } // kubernetesLabelsToPrometheusLabels generates Prometheus-safe labels from // the resource's Kubernetes labels. func kubernetesLabelsToPrometheusLabels(labels map[string]string) prometheus.Labels { promLabels := map[string]string{} for l, v := range labels { promLabels["label_"+sanitizeLabel(l)] = v } return promLabels } ================================================ FILE: pkg/statemetrics/labels_test.go ================================================ package statemetrics import ( "strings" "testing" "testing/quick" "github.com/prometheus/common/model" ) const reservedLabelPrefix = "__" func TestLabelsTransformation(t *testing.T) { f := func(labels map[string]string) bool { newLabels := kubernetesLabelsToPrometheusLabels(labels) for k := range newLabels { if !checkLabelName(k) { return false } } return true } if err := quick.Check(f, nil); err != nil { t.Error(err) } } func checkLabelName(l string) bool { return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix) } ================================================ FILE: pkg/util/conditions/conditions.go ================================================ // Adapted from https://github.com/kubernetes-sigs/cluster-api/tree/v0.3.10/util/conditions // // Copyright 2020 The Kubernetes Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package conditions provides functions for setting status conditions on Lockbox resources package conditions import ( "sort" "time" lockboxv1 "github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare.com/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type Getter interface { GetConditions() []lockboxv1.Condition } type Setter interface { Getter SetConditions([]lockboxv1.Condition) } func FalseCondition(t lockboxv1.ConditionType, reason string, severity lockboxv1.ConditionSeverity, message string) *lockboxv1.Condition { return &lockboxv1.Condition{ Type: t, Status: "False", Reason: reason, Severity: severity, Message: message, } } func TrueCondition(t lockboxv1.ConditionType) *lockboxv1.Condition { return &lockboxv1.Condition{ Type: t, Status: "True", } } func UnknownCondition(t lockboxv1.ConditionType, reason string, message string) *lockboxv1.Condition { return &lockboxv1.Condition{ Type: t, Status: "Unknown", Reason: reason, Message: message, } } func Get(from Getter, t lockboxv1.ConditionType) *lockboxv1.Condition { conditions := from.GetConditions() if conditions == nil { return nil } for _, condition := range conditions { if condition.Type == t { return &condition } } return nil } func Set(to Setter, condition *lockboxv1.Condition) { if to == nil || condition == nil { return } conditions := to.GetConditions() exists := false for i := range conditions { existingCondition := conditions[i] if existingCondition.Type == condition.Type { exists = true if !hasSameState(&existingCondition, condition) { condition.LastTransitionTime = metav1.NewTime(time.Now().UTC().Truncate(time.Second)) conditions[i] = *condition break } condition.LastTransitionTime = existingCondition.LastTransitionTime break } } if !exists { if condition.LastTransitionTime.IsZero() { condition.LastTransitionTime = metav1.NewTime(time.Now().UTC().Truncate(time.Second)) } conditions = append(conditions, *condition) } sort.Slice(conditions, func(i, j int) bool { return lexicographicLess(&conditions[i], &conditions[j]) }) to.SetConditions(conditions) } func lexicographicLess(i, j *lockboxv1.Condition) bool { return (i.Type == "Ready" || i.Type < j.Type) && j.Type != "Ready" } func hasSameState(i, j *lockboxv1.Condition) bool { return i.Type == j.Type && i.Status == j.Status && i.Reason == j.Reason && i.Severity == j.Severity && i.Message == j.Message } ================================================ FILE: tools/tools.go ================================================ //go:build tools // +build tools package tools import ( _ "sigs.k8s.io/controller-tools/cmd/controller-gen" )