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"
)
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
SYMBOL INDEX (127 symbols across 25 files)
FILE: cmd/lockbox-controller/keypair.go
type kp (line 11) | type kp struct
function KeyPairFromYAMLOrJSON (line 17) | func KeyPairFromYAMLOrJSON(r io.Reader) (pub, pri nacl.Key, err error) {
FILE: cmd/lockbox-controller/main.go
function main (line 45) | func main() {
FILE: cmd/lockbox-keypair/main.go
function main (line 12) | func main() {
FILE: cmd/locket/main.go
function main (line 41) | func main() {
function GetConfig (line 189) | func GetConfig() clientcmd.ClientConfig {
function GetRemotePublicKey (line 200) | func GetRemotePublicKey(ctx context.Context, c kubernetes.Interface, ns,...
FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/groupversion_info.go
function init (line 25) | func init() {
FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox.go
constant keySize (line 10) | keySize = nacl.KeySize
function NewFromSecret (line 14) | func NewFromSecret(secret corev1.Secret, namespace string, peer, pub, pr...
method UnlockInto (line 51) | func (in *Lockbox) UnlockInto(secret *corev1.Secret, pri nacl.Key) error {
type decryptSecretKeyError (line 75) | type decryptSecretKeyError struct
method SecretKey (line 81) | func (e decryptSecretKeyError) SecretKey() string {
method Unwrap (line 86) | func (e decryptSecretKeyError) Unwrap() error {
method GetConditions (line 90) | func (in *Lockbox) GetConditions() []Condition {
method SetConditions (line 94) | func (in *Lockbox) SetConditions(conditions []Condition) {
FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox_test.go
function TestUnlock (line 15) | func TestUnlock(t *testing.T) {
function TestUnlockErr (line 71) | func TestUnlockErr(t *testing.T) {
function TestLockUnlock (line 115) | func TestLockUnlock(t *testing.T) {
function loadKeypair (line 147) | func loadKeypair(t *testing.T, pub, pri string) (pubKey, priKey nacl.Key...
FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/types.go
type Lockbox (line 15) | type Lockbox struct
type LockboxSpec (line 29) | type LockboxSpec struct
type LockboxSecretTemplate (line 53) | type LockboxSecretTemplate struct
type LockboxSecretTemplateMetadata (line 60) | type LockboxSecretTemplateMetadata struct
type LockboxStatus (line 77) | type LockboxStatus struct
type Condition (line 84) | type Condition struct
type ConditionType (line 116) | type ConditionType
constant ReadyCondition (line 119) | ReadyCondition ConditionType = "Ready"
type ConditionSeverity (line 123) | type ConditionSeverity
constant ConditionSeverityError (line 126) | ConditionSeverityError ConditionSeverity = "Error"
constant ConditionSeverityWarning (line 127) | ConditionSeverityWarning ConditionSeverity = "Warning"
constant ConditionSeverityInfo (line 128) | ConditionSeverityInfo ConditionSeverity = "Info"
constant ConditionSeverityNone (line 129) | ConditionSeverityNone ConditionSeverity = ""
type LockboxList (line 135) | type LockboxList struct
FILE: pkg/apis/lockbox.k8s.cloudflare.com/v1/zz_generated.deepcopy.go
method DeepCopyInto (line 12) | func (in *Condition) DeepCopyInto(out *Condition) {
method DeepCopy (line 18) | func (in *Condition) DeepCopy() *Condition {
method DeepCopyInto (line 28) | func (in *Lockbox) DeepCopyInto(out *Lockbox) {
method DeepCopy (line 37) | func (in *Lockbox) DeepCopy() *Lockbox {
method DeepCopyObject (line 47) | func (in *Lockbox) DeepCopyObject() runtime.Object {
method DeepCopyInto (line 55) | func (in *LockboxList) DeepCopyInto(out *LockboxList) {
method DeepCopy (line 69) | func (in *LockboxList) DeepCopy() *LockboxList {
method DeepCopyObject (line 79) | func (in *LockboxList) DeepCopyObject() runtime.Object {
method DeepCopyInto (line 87) | func (in *LockboxSecretTemplate) DeepCopyInto(out *LockboxSecretTemplate) {
method DeepCopy (line 93) | func (in *LockboxSecretTemplate) DeepCopy() *LockboxSecretTemplate {
method DeepCopyInto (line 103) | func (in *LockboxSecretTemplateMetadata) DeepCopyInto(out *LockboxSecret...
method DeepCopy (line 122) | func (in *LockboxSecretTemplateMetadata) DeepCopy() *LockboxSecretTempla...
method DeepCopyInto (line 132) | func (in *LockboxSpec) DeepCopyInto(out *LockboxSpec) {
method DeepCopy (line 169) | func (in *LockboxSpec) DeepCopy() *LockboxSpec {
method DeepCopyInto (line 179) | func (in *LockboxStatus) DeepCopyInto(out *LockboxStatus) {
method DeepCopy (line 191) | func (in *LockboxStatus) DeepCopy() *LockboxStatus {
FILE: pkg/flagvar/enum.go
type Enum (line 11) | type Enum struct
method Help (line 16) | func (e *Enum) Help() string {
method Set (line 20) | func (e *Enum) Set(v string) error {
method String (line 31) | func (e *Enum) String() string {
FILE: pkg/flagvar/enum_test.go
function TestEnumString (line 10) | func TestEnumString(t *testing.T) {
function TestEnumSet (line 43) | func TestEnumSet(t *testing.T) {
FILE: pkg/flagvar/file.go
type File (line 8) | type File struct
method Help (line 13) | func (f *File) Help() string {
method Set (line 19) | func (f *File) Set(v string) error {
method String (line 27) | func (f *File) String() string {
method Type (line 36) | func (f *File) Type() string {
FILE: pkg/flagvar/file_test.go
function TestFileString (line 12) | func TestFileString(t *testing.T) {
function TestFileSet (line 45) | func TestFileSet(t *testing.T) {
FILE: pkg/flagvar/tcp_addr.go
type TCPAddr (line 6) | type TCPAddr struct
method Help (line 13) | func (t *TCPAddr) Help() string {
method Set (line 19) | func (t *TCPAddr) Set(v string) error {
method String (line 33) | func (t *TCPAddr) String() string {
method Type (line 42) | func (t *TCPAddr) Type() string {
FILE: pkg/flagvar/tcp_addr_test.go
function TestTCPAddrString (line 11) | func TestTCPAddrString(t *testing.T) {
function TestTCPAddrSet (line 44) | func TestTCPAddrSet(t *testing.T) {
FILE: pkg/lockbox-controller/secretreconciler.go
constant keySize (line 28) | keySize = nacl.KeySize
type SecretReconcilerOption (line 31) | type SecretReconcilerOption
type SecretReconciler (line 34) | type SecretReconciler struct
method Reconcile (line 60) | func (s *SecretReconciler) Reconcile(ctx context.Context, lb *lockboxv...
method reconcileExisting (line 138) | func (s *SecretReconciler) reconcileExisting(lb *lockboxv1.Lockbox, se...
function NewSecretReconciler (line 44) | func NewSecretReconciler(pubKey, priKey nacl.Key, options ...SecretRecon...
function WithRecorder (line 156) | func WithRecorder(r record.EventRecorder) SecretReconcilerOption {
function WithClient (line 163) | func WithClient(c client.Client) SecretReconcilerOption {
type decryptSecretKeyErrorer (line 171) | type decryptSecretKeyErrorer interface
FILE: pkg/lockbox-controller/secretreconciler_suite_test.go
function TestMain (line 38) | func TestMain(m *testing.M) {
function TestSuiteSecretReconciler (line 56) | func TestSuiteSecretReconciler(t *testing.T) {
FILE: pkg/lockbox-controller/secretreconciler_test.go
function TestSecretReconciler (line 22) | func TestSecretReconciler(t *testing.T) {
function loadKeypair (line 318) | func loadKeypair(t *testing.T, pub, pri string) (pubKey, priKey nacl.Key...
FILE: pkg/lockbox-server/serve.go
function PublicKey (line 11) | func PublicKey(pubKey nacl.Key) http.Handler {
FILE: pkg/statemetrics/collector.go
type Kubernetes (line 11) | type Kubernetes interface
type kubernetes (line 20) | type kubernetes struct
method Set (line 27) | func (k *kubernetes) Set(val float64) {
method Describe (line 32) | func (k *kubernetes) Describe(ch chan<- *prometheus.Desc) {
method Collect (line 37) | func (k *kubernetes) Collect(ch chan<- prometheus.Metric) {
type KubernetesVec (line 45) | type KubernetesVec struct
method WithLabelValues (line 74) | func (v *KubernetesVec) WithLabelValues(uid types.UID, lvs ...string) ...
method Delete (line 89) | func (v *KubernetesVec) Delete(uid types.UID) {
method Describe (line 97) | func (v *KubernetesVec) Describe(ch chan<- *prometheus.Desc) {
method Collect (line 102) | func (v *KubernetesVec) Collect(ch chan<- prometheus.Metric) {
type KubernetesOpts (line 52) | type KubernetesOpts
function NewKubernetesVec (line 56) | func NewKubernetesVec(opts KubernetesOpts, labelNames []string) *Kuberne...
type LabelsVec (line 115) | type LabelsVec struct
method With (line 132) | func (v *LabelsVec) With(uid types.UID, l prometheus.Labels) Kubernetes {
method Delete (line 162) | func (v *LabelsVec) Delete(uid types.UID) {
method Describe (line 171) | func (v *LabelsVec) Describe(chan<- *prometheus.Desc) {}
method Collect (line 174) | func (v *LabelsVec) Collect(ch chan<- prometheus.Metric) {
function NewLabelsVec (line 122) | func NewLabelsVec(opts KubernetesOpts) *LabelsVec {
FILE: pkg/statemetrics/handler.go
type StateMetricProxy (line 15) | type StateMetricProxy struct
method Create (line 40) | func (s *StateMetricProxy) Create(ctx context.Context, evt event.Creat...
method Update (line 49) | func (s *StateMetricProxy) Update(ctx context.Context, evt event.Updat...
method Delete (line 58) | func (s *StateMetricProxy) Delete(ctx context.Context, evt event.Delet...
method Generic (line 74) | func (s *StateMetricProxy) Generic(ctx context.Context, evt event.Gene...
method updateWith (line 81) | func (s *StateMetricProxy) updateWith(obj client.Object) {
function NewStateMetricProxy (line 27) | func NewStateMetricProxy(enqueuer handler.EventHandler, info, created, r...
FILE: pkg/statemetrics/handler_test.go
function TestStateMetricsProxy_Create (line 16) | func TestStateMetricsProxy_Create(t *testing.T) {
function TestStateMetricsProxy_Update (line 71) | func TestStateMetricsProxy_Update(t *testing.T) {
function TestStateMetricsProxy_Delete (line 149) | func TestStateMetricsProxy_Delete(t *testing.T) {
function createMetricVectors (line 190) | func createMetricVectors(t *testing.T) (info, created, resourceVersion, ...
FILE: pkg/statemetrics/labels.go
function sanitizeLabel (line 12) | func sanitizeLabel(l string) string {
function kubernetesLabelsToPrometheusLabels (line 18) | func kubernetesLabelsToPrometheusLabels(labels map[string]string) promet...
FILE: pkg/statemetrics/labels_test.go
constant reservedLabelPrefix (line 11) | reservedLabelPrefix = "__"
function TestLabelsTransformation (line 13) | func TestLabelsTransformation(t *testing.T) {
function checkLabelName (line 31) | func checkLabelName(l string) bool {
FILE: pkg/util/conditions/conditions.go
type Getter (line 28) | type Getter interface
type Setter (line 32) | type Setter interface
function FalseCondition (line 37) | func FalseCondition(t lockboxv1.ConditionType, reason string, severity l...
function TrueCondition (line 47) | func TrueCondition(t lockboxv1.ConditionType) *lockboxv1.Condition {
function UnknownCondition (line 54) | func UnknownCondition(t lockboxv1.ConditionType, reason string, message ...
function Get (line 63) | func Get(from Getter, t lockboxv1.ConditionType) *lockboxv1.Condition {
function Set (line 78) | func Set(to Setter, condition *lockboxv1.Condition) {
function lexicographicLess (line 113) | func lexicographicLess(i, j *lockboxv1.Condition) bool {
function hasSameState (line 117) | func hasSameState(i, j *lockboxv1.Condition) bool {
Condensed preview — 46 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (145K chars).
[
{
"path": ".dockerignore",
"chars": 5,
"preview": "bin/\n"
},
{
"path": ".github/workflows/docker.yaml",
"chars": 816,
"preview": "name: Docker\non:\n - pull_request\n - push\njobs:\n docker:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/c"
},
{
"path": ".github/workflows/semgrep.yml",
"chars": 571,
"preview": "on:\n pull_request: {}\n workflow_dispatch: {}\n push:\n branches:\n - trunk\n schedule:\n - cron: \"0 0 * * *\"\nn"
},
{
"path": ".github/workflows/tests.yaml",
"chars": 918,
"preview": "name: Test\non:\n - pull_request\n - push\njobs:\n unit:\n runs-on: ubuntu-latest\n name: \"Go ${{ matrix.go }} Test\"\n "
},
{
"path": ".gitignore",
"chars": 200,
"preview": "## Go.gitignore ##\n\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go t"
},
{
"path": "LICENSE",
"chars": 1525,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2020, Cloudflare, Inc.\nAll rights reserved.\n\nRedistribution and use in source and bi"
},
{
"path": "Makefile",
"chars": 2560,
"preview": ".DEFAULT_GOAL := binaries\n\nKERNEL := $(shell uname -s)\nGOTESTSUM := $(shell command -v gotestsum 2> /dev/null)\n\nDIB ?= d"
},
{
"path": "README.org",
"chars": 1206,
"preview": "#+TITLE: Lockbox\n\n[[https://pkg.go.dev/github.com/cloudflare/lockbox][https://pkg.go.dev/badge/github.com/cloudflare/loc"
},
{
"path": "cmd/lockbox-controller/Dockerfile",
"chars": 462,
"preview": "FROM docker.io/library/golang:1.21.5-bookworm AS builder\nWORKDIR /go/src/app\nADD . /go/src/app\n\nRUN --mount=type=cache,t"
},
{
"path": "cmd/lockbox-controller/keypair.go",
"chars": 910,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/kevinburke/nacl\"\n\t\"sigs.k8s.io/yaml\"\n)\n\ntype kp struct {\n\tPrivate []by"
},
{
"path": "cmd/lockbox-controller/main.go",
"chars": 6394,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n\n\tlockboxv1 \"github.com/clo"
},
{
"path": "cmd/lockbox-keypair/main.go",
"chars": 418,
"preview": "package main\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/kevinburke/nacl/box\"\n)\n\nfunc main() "
},
{
"path": "cmd/locket/main.go",
"chars": 5532,
"preview": "package main\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\tgruntime \"runtime\"\n\t\"time\"\n\n\tlockboxv1 \"git"
},
{
"path": "deployment/crds/lockbox.k8s.cloudflare.com_lockboxes.yaml",
"chars": 6717,
"preview": "---\napiVersion: apiextensions.k8s.io/v1\nkind: CustomResourceDefinition\nmetadata:\n annotations:\n controller-gen.kubeb"
},
{
"path": "deployment/manifests/deployment-lockbox.yaml",
"chars": 763,
"preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: lockbox-controller\n namespace: lockbox\nspec:\n replicas: 1\n sel"
},
{
"path": "deployment/manifests/namespace-lockbox.yaml",
"chars": 57,
"preview": "apiVersion: v1\nkind: Namespace\nmetadata:\n name: lockbox\n"
},
{
"path": "deployment/manifests/service-lockbox.yaml",
"chars": 180,
"preview": "kind: Service\napiVersion: v1\nmetadata:\n name: lockbox\n namespace: lockbox\nspec:\n ports:\n - port: 80\n targetPort: "
},
{
"path": "deployment/manifests/serviceaccount-lockbox.yaml",
"chars": 94,
"preview": "apiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: lockbox-controller\n namespace: lockbox\n"
},
{
"path": "deployment/rbac/proxier.yaml",
"chars": 507,
"preview": "apiVersion: rbac.authorization.k8s.io/v1\nkind: Role\nmetadata:\n name: lockbox-proxier\n namespace: lockbox\nrules:\n - ap"
},
{
"path": "deployment/rbac/role-binding.yaml",
"chars": 284,
"preview": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n name: lockbox-controller\nroleRef:\n apiGro"
},
{
"path": "deployment/rbac/role.yaml",
"chars": 517,
"preview": "---\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole\nmetadata:\n name: lockbox-controller\nrules:\n- apiGroups:\n"
},
{
"path": "go.mod",
"chars": 3404,
"preview": "module github.com/cloudflare/lockbox\n\ngo 1.21\n\ntoolchain go1.21.5\n\nrequire (\n\tgithub.com/go-logr/zerologr v1.2.3\n\tgithub"
},
{
"path": "go.sum",
"chars": 22213,
"preview": "github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:"
},
{
"path": "pkg/apis/lockbox.k8s.cloudflare.com/v1/groupversion_info.go",
"chars": 829,
"preview": "// +kubebuilder:object:generate=true\n// +groupName=lockbox.k8s.cloudflare.com\n\n// Package v1 is the v1 version of the Lo"
},
{
"path": "pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox.go",
"chars": 2449,
"preview": "package v1\n\nimport (\n\t\"github.com/kevinburke/nacl\"\n\t\"github.com/kevinburke/nacl/box\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav"
},
{
"path": "pkg/apis/lockbox.k8s.cloudflare.com/v1/lockbox_test.go",
"chars": 6091,
"preview": "package v1_test\n\nimport (\n\t\"crypto/rand\"\n\t\"testing\"\n\n\tv1 \"github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s.cloudflare."
},
{
"path": "pkg/apis/lockbox.k8s.cloudflare.com/v1/types.go",
"chars": 4686,
"preview": "package v1\n\nimport (\n\tcorev1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// +kubebuilder:obje"
},
{
"path": "pkg/apis/lockbox.k8s.cloudflare.com/v1/zz_generated.deepcopy.go",
"chars": 5520,
"preview": "//go:build !ignore_autogenerated\n\n// Code generated by controller-gen. DO NOT EDIT.\n\npackage v1\n\nimport (\n\truntime \"k8s."
},
{
"path": "pkg/flagvar/enum.go",
"chars": 508,
"preview": "package flagvar\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nvar ErrInvalidEnum = errors.New(\"invalid enum option\")\n\ntype En"
},
{
"path": "pkg/flagvar/enum_test.go",
"chars": 1458,
"preview": "package flagvar_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/cloudflare/lockbox/pkg/flagvar\"\n\t\"gotest.tools/v3/assert\"\n)\n\nfun"
},
{
"path": "pkg/flagvar/file.go",
"chars": 771,
"preview": "package flagvar\n\nimport (\n\t\"os\"\n)\n\n// File is a flag.Value for file paths. Returns any errors from os.Stat.\ntype File st"
},
{
"path": "pkg/flagvar/file_test.go",
"chars": 1446,
"preview": "package flagvar_test\n\nimport (\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/cloudflare/lockbox/pkg/flagvar\"\n\t\"gote"
},
{
"path": "pkg/flagvar/tcp_addr.go",
"chars": 957,
"preview": "package flagvar\n\nimport \"net\"\n\n// TCPAddr is a flag.Value for file paths. Returns any errors from net.ResolveTCPAddr.\nty"
},
{
"path": "pkg/flagvar/tcp_addr_test.go",
"chars": 1718,
"preview": "package flagvar_test\n\nimport (\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/cloudflare/lockbox/pkg/flagvar\"\n\t\"gotest.tools/v3/assert\""
},
{
"path": "pkg/flagvar/testdata/file",
"chars": 0,
"preview": ""
},
{
"path": "pkg/lockbox-controller/secretreconciler.go",
"chars": 6574,
"preview": "package controller\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\tlockboxv1 \"github.com/cloudflare/lockbox/pkg/apis/lo"
},
{
"path": "pkg/lockbox-controller/secretreconciler_suite_test.go",
"chars": 8516,
"preview": "//go:build suite\n// +build suite\n\npackage controller_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"te"
},
{
"path": "pkg/lockbox-controller/secretreconciler_test.go",
"chars": 14146,
"preview": "package controller_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tlockboxv1 \"github.com/cloudflare/lockbox/pkg/apis/lockbox.k8s."
},
{
"path": "pkg/lockbox-server/serve.go",
"chars": 386,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/kevinburke/nacl\"\n)\n\n// PublicKey creates an HTTP handler that respons"
},
{
"path": "pkg/statemetrics/collector.go",
"chars": 4505,
"preview": "package statemetrics\n\nimport (\n\t\"sync\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n\t\"k8s.io/apimachinery/pkg/type"
},
{
"path": "pkg/statemetrics/handler.go",
"chars": 3158,
"preview": "package statemetrics\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\n\tlockboxv1 \"github.com/cloudflare/lockbox/pkg/apis/lockbox.k8"
},
{
"path": "pkg/statemetrics/handler_test.go",
"chars": 7268,
"preview": "package statemetrics\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tlockboxv1 \"github.com/cloudflare/lockbox/pkg/a"
},
{
"path": "pkg/statemetrics/labels.go",
"chars": 653,
"preview": "package statemetrics\n\nimport (\n\t\"regexp\"\n\n\t\"github.com/prometheus/client_golang/prometheus\"\n)\n\nvar invalidLabelCharRE = "
},
{
"path": "pkg/statemetrics/labels_test.go",
"chars": 584,
"preview": "package statemetrics\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\t\"testing/quick\"\n\n\t\"github.com/prometheus/common/model\"\n)\n\nconst re"
},
{
"path": "pkg/util/conditions/conditions.go",
"chars": 3213,
"preview": "// Adapted from https://github.com/kubernetes-sigs/cluster-api/tree/v0.3.10/util/conditions\n//\n// Copyright 2020 The Kub"
},
{
"path": "tools/tools.go",
"chars": 113,
"preview": "//go:build tools\n// +build tools\n\npackage tools\n\nimport (\n\t_ \"sigs.k8s.io/controller-tools/cmd/controller-gen\"\n)\n"
}
]
About this extraction
This page contains the full source code of the cloudflare/lockbox GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 46 files (128.7 KB), approximately 49.5k tokens, and a symbol index with 127 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.