From 1a085562e41004848a6a664bd05374801bd1324d Mon Sep 17 00:00:00 2001 From: Marton Soos Date: Mon, 11 Apr 2022 10:06:24 +0200 Subject: [PATCH] Migrate controllers and helper code from pkg --- controllers/deployment_controller.go | 235 +++++ controllers/onepassworditem_controller.go | 143 ++- go.mod | 21 +- go.sum | 82 +- .../kubernetes_secrets_builder.go | 185 ++++ .../kubernetes_secrets_builder_test.go | 313 +++++++ pkg/mocks/mocksecretserver.go | 84 ++ pkg/onepassword/annotations.go | 60 ++ pkg/onepassword/annotations_test.go | 93 ++ pkg/onepassword/connect_setup.go | 117 +++ pkg/onepassword/connect_setup_test.go | 65 ++ pkg/onepassword/containers.go | 53 ++ pkg/onepassword/containers_test.go | 85 ++ pkg/onepassword/deployments.go | 26 + pkg/onepassword/deployments_test.go | 58 ++ pkg/onepassword/items.go | 100 ++ pkg/onepassword/object_generators_for_test.go | 54 ++ pkg/onepassword/secret_update_handler.go | 248 +++++ pkg/onepassword/secret_update_handler_test.go | 860 ++++++++++++++++++ pkg/onepassword/uuid.go | 20 + pkg/onepassword/volumes.go | 29 + pkg/onepassword/volumes_test.go | 43 + pkg/utils/string.go | 33 + 23 files changed, 2982 insertions(+), 25 deletions(-) create mode 100644 controllers/deployment_controller.go create mode 100644 pkg/kubernetessecrets/kubernetes_secrets_builder.go create mode 100644 pkg/kubernetessecrets/kubernetes_secrets_builder_test.go create mode 100644 pkg/mocks/mocksecretserver.go create mode 100644 pkg/onepassword/annotations.go create mode 100644 pkg/onepassword/annotations_test.go create mode 100644 pkg/onepassword/connect_setup.go create mode 100644 pkg/onepassword/connect_setup_test.go create mode 100644 pkg/onepassword/containers.go create mode 100644 pkg/onepassword/containers_test.go create mode 100644 pkg/onepassword/deployments.go create mode 100644 pkg/onepassword/deployments_test.go create mode 100644 pkg/onepassword/items.go create mode 100644 pkg/onepassword/object_generators_for_test.go create mode 100644 pkg/onepassword/secret_update_handler.go create mode 100644 pkg/onepassword/secret_update_handler_test.go create mode 100644 pkg/onepassword/uuid.go create mode 100644 pkg/onepassword/volumes.go create mode 100644 pkg/onepassword/volumes_test.go create mode 100644 pkg/utils/string.go diff --git a/controllers/deployment_controller.go b/controllers/deployment_controller.go new file mode 100644 index 0000000..855d7b0 --- /dev/null +++ b/controllers/deployment_controller.go @@ -0,0 +1,235 @@ +package controllers + +import ( + "context" + "fmt" + + kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" + op "github.com/1Password/onepassword-operator/pkg/onepassword" + "github.com/1Password/onepassword-operator/pkg/utils" + + "regexp" + + "github.com/1Password/connect-sdk-go/connect" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "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/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var deploymentLog = logf.Log.WithName("controller_deployment") +var finalizer = "onepassword.com/finalizer.secret" + +const annotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" + +func Add(mgr manager.Manager, opConnectClient connect.Client) error { + return add(mgr, newReconciler(mgr, opConnectClient)) +} + +func newReconciler(mgr manager.Manager, opConnectClient connect.Client) *ReconcileDeployment { + r, _ := regexp.Compile(annotationRegExpString) + return &ReconcileDeployment{ + opAnnotationRegExp: r, + kubeClient: mgr.GetClient(), + scheme: mgr.GetScheme(), + opConnectClient: opConnectClient, + } +} + +func add(mgr manager.Manager, r reconcile.Reconciler) error { + c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource Deployment + err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + return nil +} + +var _ reconcile.Reconciler = &ReconcileDeployment{} + +type ReconcileDeployment struct { + opAnnotationRegExp *regexp.Regexp + kubeClient client.Client + scheme *runtime.Scheme + opConnectClient connect.Client +} + +func (r *ReconcileDeployment) SetupWithManager(mgr ctrl.Manager) error { + + c, err := controller.New("deployment-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource Deployment + err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + return nil + // TODO figure out what to do with this code. + // return ctrl.NewControllerManagedBy(mgr). + // For(&appsv1.Deployment{}). + // Complete(r) +} + +func (r *ReconcileDeployment) test() { + return +} + +// Reconcile reads that state of the cluster for a Deployment object and makes changes based on the state read +// and what is in the Deployment.Spec +// Note: +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *ReconcileDeployment) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { + reqLogger := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + reqLogger.Info("Reconciling Deployment") + + deployment := &appsv1.Deployment{} + err := r.kubeClient.Get(ctx, request.NamespacedName, deployment) + if err != nil { + if errors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, err + } + + annotations, annotationsFound := op.GetAnnotationsForDeployment(deployment, r.opAnnotationRegExp) + if !annotationsFound { + reqLogger.Info("No 1Password Annotations found") + return reconcile.Result{}, nil + } + + //If the deployment is not being deleted + if deployment.ObjectMeta.DeletionTimestamp.IsZero() { + // Adds a finalizer to the deployment if one does not exist. + // This is so we can handle cleanup of associated secrets properly + if !utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) { + deployment.ObjectMeta.Finalizers = append(deployment.ObjectMeta.Finalizers, finalizer) + if err := r.kubeClient.Update(context.Background(), deployment); err != nil { + return reconcile.Result{}, err + } + } + // Handles creation or updating secrets for deployment if needed + if err := r.HandleApplyingDeployment(deployment, deployment.Namespace, annotations, request); err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{}, nil + } + // The deployment has been marked for deletion. If the one password + // finalizer is found there are cleanup tasks to perform + if utils.ContainsString(deployment.ObjectMeta.Finalizers, finalizer) { + + secretName := annotations[op.NameAnnotation] + r.cleanupKubernetesSecretForDeployment(secretName, deployment) + + // Remove the finalizer from the deployment so deletion of deployment can be completed + if err := r.removeOnePasswordFinalizerFromDeployment(deployment); err != nil { + return reconcile.Result{}, err + } + } + return reconcile.Result{}, nil +} + +func (r *ReconcileDeployment) cleanupKubernetesSecretForDeployment(secretName string, deletedDeployment *appsv1.Deployment) error { + kubernetesSecret := &corev1.Secret{} + kubernetesSecret.ObjectMeta.Name = secretName + kubernetesSecret.ObjectMeta.Namespace = deletedDeployment.Namespace + + if len(secretName) == 0 { + return nil + } + updatedSecrets := map[string]*corev1.Secret{secretName: kubernetesSecret} + + multipleDeploymentsUsingSecret, err := r.areMultipleDeploymentsUsingSecret(updatedSecrets, *deletedDeployment) + if err != nil { + return err + } + + // Only delete the associated kubernetes secret if it is not being used by other deployments + if !multipleDeploymentsUsingSecret { + if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil { + if !errors.IsNotFound(err) { + return err + } + } + } + return nil +} + +func (r *ReconcileDeployment) areMultipleDeploymentsUsingSecret(updatedSecrets map[string]*corev1.Secret, deletedDeployment appsv1.Deployment) (bool, error) { + deployments := &appsv1.DeploymentList{} + opts := []client.ListOption{ + client.InNamespace(deletedDeployment.Namespace), + } + + err := r.kubeClient.List(context.Background(), deployments, opts...) + if err != nil { + deploymentLog.Error(err, "Failed to list kubernetes deployments") + return false, err + } + + for i := 0; i < len(deployments.Items); i++ { + if deployments.Items[i].Name != deletedDeployment.Name { + if op.IsDeploymentUsingSecrets(&deployments.Items[i], updatedSecrets) { + return true, nil + } + } + } + return false, nil +} + +func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deployment *appsv1.Deployment) error { + deployment.ObjectMeta.Finalizers = utils.RemoveString(deployment.ObjectMeta.Finalizers, finalizer) + return r.kubeClient.Update(context.Background(), deployment) +} + +func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error { + reqLog := deploymentLog.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + + secretName := annotations[op.NameAnnotation] + secretLabels := map[string]string(nil) + secretType := "" + + if len(secretName) == 0 { + reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.") + return nil + } + + item, err := op.GetOnePasswordItemByPath(r.opConnectClient, annotations[op.ItemPathAnnotation]) + if err != nil { + return fmt.Errorf("Failed to retrieve item: %v", err) + } + + // Create owner reference. + gvk, err := apiutil.GVKForObject(deployment, r.scheme) + if err != nil { + return fmt.Errorf("could not to retrieve group version kind: %v", err) + } + ownerRef := &metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: deployment.GetName(), + UID: deployment.GetUID(), + } + + return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, annotations, ownerRef) +} diff --git a/controllers/onepassworditem_controller.go b/controllers/onepassworditem_controller.go index e914321..d73ecdc 100644 --- a/controllers/onepassworditem_controller.go +++ b/controllers/onepassworditem_controller.go @@ -18,19 +18,40 @@ package controllers import ( "context" + "fmt" + "github.com/1Password/onepassword-operator/pkg/onepassword" + op "github.com/1Password/onepassword-operator/pkg/onepassword" + + kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + kubeClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/source" + + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/1Password/onepassword-operator/pkg/utils" + + "github.com/1Password/connect-sdk-go/connect" + corev1 "k8s.io/api/core/v1" onepasswordv1 "github.com/1Password/onepassword-operator/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +var log = logf.Log.WithName("controller_onepassworditem") + // OnePasswordItemReconciler reconciles a OnePasswordItem object type OnePasswordItemReconciler struct { - client.Client - Scheme *runtime.Scheme + kubeClient kubeClient.Client + scheme *runtime.Scheme + opConnectClient connect.Client } //+kubebuilder:rbac:groups=onepassword.onepassword.com,resources=onepassworditems,verbs=get;list;watch;create;update;patch;delete @@ -46,17 +67,117 @@ type OnePasswordItemReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile -func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) +func (r *OnePasswordItemReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { + reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + reqLogger.Info("Reconciling OnePasswordItem") - // TODO(user): your logic here + onepassworditem := &onepasswordv1.OnePasswordItem{} + err := r.kubeClient.Get(context.Background(), request.NamespacedName, onepassworditem) + if err != nil { + if errors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, err + } - return ctrl.Result{}, nil + // If the deployment is not being deleted + if onepassworditem.ObjectMeta.DeletionTimestamp.IsZero() { + // Adds a finalizer to the deployment if one does not exist. + // This is so we can handle cleanup of associated secrets properly + if !utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { + onepassworditem.ObjectMeta.Finalizers = append(onepassworditem.ObjectMeta.Finalizers, finalizer) + if err := r.kubeClient.Update(context.Background(), onepassworditem); err != nil { + return reconcile.Result{}, err + } + } + + // Handles creation or updating secrets for deployment if needed + if err := r.HandleOnePasswordItem(onepassworditem, request); err != nil { + return reconcile.Result{}, err + } + return reconcile.Result{}, nil + } + // If one password finalizer exists then we must cleanup associated secrets + if utils.ContainsString(onepassworditem.ObjectMeta.Finalizers, finalizer) { + + // Delete associated kubernetes secret + if err = r.cleanupKubernetesSecret(onepassworditem); err != nil { + return reconcile.Result{}, err + } + + // Remove finalizer now that cleanup is complete + if err := r.removeFinalizer(onepassworditem); err != nil { + return reconcile.Result{}, err + } + } + return reconcile.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *OnePasswordItemReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&onepasswordv1.OnePasswordItem{}). - Complete(r) + c, err := controller.New("onepassworditem-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource OnePasswordItem + err = c.Watch(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + return nil + // TODO Consider the simplified code below. Based on the migration guide: https://sdk.operatorframework.io/docs/building-operators/golang/migration/#create-a-new-project + // return ctrl.NewControllerManagedBy(mgr).Named("onepassworditem-controller").WithOptions(controller.Options{Reconciler: r}). + // For(&onepasswordv1.OnePasswordItem{}).Watches(&source.Kind{Type: &onepasswordv1.OnePasswordItem{}}, &handler.EnqueueRequestForObject{}). + // Complete(r) +} + +func (r *OnePasswordItemReconciler) removeFinalizer(onePasswordItem *onepasswordv1.OnePasswordItem) error { + onePasswordItem.ObjectMeta.Finalizers = utils.RemoveString(onePasswordItem.ObjectMeta.Finalizers, finalizer) + if err := r.kubeClient.Update(context.Background(), onePasswordItem); err != nil { + return err + } + return nil +} + +func (r *OnePasswordItemReconciler) cleanupKubernetesSecret(onePasswordItem *onepasswordv1.OnePasswordItem) error { + kubernetesSecret := &corev1.Secret{} + kubernetesSecret.ObjectMeta.Name = onePasswordItem.Name + kubernetesSecret.ObjectMeta.Namespace = onePasswordItem.Namespace + + r.kubeClient.Delete(context.Background(), kubernetesSecret) + if err := r.kubeClient.Delete(context.Background(), kubernetesSecret); err != nil { + if !errors.IsNotFound(err) { + return err + } + } + return nil +} + +func (r *OnePasswordItemReconciler) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { + secretName := resource.GetName() + labels := resource.Labels + annotations := resource.Annotations + secretType := resource.Type + autoRestart := annotations[op.RestartDeploymentsAnnotation] + + item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath) + if err != nil { + return fmt.Errorf("Failed to retrieve item: %v", err) + } + + // Create owner reference. + gvk, err := apiutil.GVKForObject(resource, r.scheme) + if err != nil { + return fmt.Errorf("could not to retrieve group version kind: %v", err) + } + ownerRef := &metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + Name: resource.GetName(), + UID: resource.GetUID(), + } + + return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, secretType, annotations, ownerRef) } diff --git a/go.mod b/go.mod index 6cb459a..bce61b7 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,14 @@ module github.com/1Password/onepassword-operator go 1.17 require ( + github.com/1Password/connect-sdk-go v1.2.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.17.0 - k8s.io/apimachinery v0.23.0 - k8s.io/client-go v0.23.0 + github.com/stretchr/testify v1.7.0 + k8s.io/api v0.23.5 + k8s.io/apimachinery v0.23.5 + k8s.io/client-go v0.23.5 + k8s.io/kubectl v0.23.5 sigs.k8s.io/controller-runtime v0.11.0 ) @@ -39,17 +43,21 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nxadm/tail v1.4.8 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.11.0 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.28.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect + github.com/uber/jaeger-lib v2.4.0+incompatible // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.19.1 // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect + golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect @@ -62,13 +70,12 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/api v0.23.0 // indirect k8s.io/apiextensions-apiserver v0.23.0 // indirect - k8s.io/component-base v0.23.0 // indirect + k8s.io/component-base v0.23.5 // indirect k8s.io/klog/v2 v2.30.0 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect + k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index c7f720b..9df6f77 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/1Password/connect-sdk-go v1.2.0 h1:WbIvmbDUpA89nyH0l3LF2iRSFJAv86d2D7IjVNjw6iw= +github.com/1Password/connect-sdk-go v1.2.0/go.mod h1:qK2bF/GweAq812xj+HGfbauaE6cKX1MXfKhpAvoHEq8= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= @@ -56,6 +58,9 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/HdrHistogram/hdrhistogram-go v1.0.1 h1:GX8GAYDuhlFQnI2fRDHQhTlkHMz8bEn0jTI6LJU0mpw= +github.com/HdrHistogram/hdrhistogram-go v1.0.1/go.mod h1:BWJ+nMSHY3L41Zj7CA3uXnloDp7xxV0YvstAE7nKTaM= +github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -69,6 +74,7 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= @@ -90,6 +96,7 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -102,8 +109,10 @@ github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -114,8 +123,10 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ 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/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= @@ -130,8 +141,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -141,9 +155,11 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -207,6 +223,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= @@ -242,6 +259,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -251,6 +270,7 @@ github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9 github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -314,13 +334,18 @@ 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/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -329,6 +354,7 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -343,6 +369,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -354,6 +381,7 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= @@ -366,7 +394,10 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -408,9 +439,11 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -427,18 +460,23 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -449,7 +487,14 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U= +github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.0+incompatible h1:fY7QsGQWiCt8pajv4r7JEvmATdCVaWxXbjwyYwsNaLQ= +github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -484,6 +529,7 @@ go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -558,6 +604,7 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -591,8 +638,9 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= +golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -637,6 +685,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -847,6 +896,7 @@ google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxH google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -921,28 +971,41 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.23.0 h1:WrL1gb73VSC8obi8cuYETJGXEoFNEh3LU0Pt+Sokgro= k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= +k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= +k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= k8s.io/apiextensions-apiserver v0.23.0 h1:uii8BYmHYiT2ZTAJxmvc3X8UhNYMxl2A0z0Xq3Pm+WY= k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= -k8s.io/apimachinery v0.23.0 h1:mIfWRMjBuMdolAWJ3Fd+aPTMv3X9z+waiARMpvvb0HQ= k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= +k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= +k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= -k8s.io/client-go v0.23.0 h1:vcsOqyPq7XV3QmQRCBH/t9BICJM9Q1M18qahjv+rebY= +k8s.io/cli-runtime v0.23.5/go.mod h1:oY6QDF2qo9xndSq32tqcmRp2UyXssdGrLfjAVymgbx4= k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= +k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= +k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= -k8s.io/component-base v0.23.0 h1:UAnyzjvVZ2ZR1lF35YwtNY6VMN94WtOnArcXBu34es8= +k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= +k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= +k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= +k8s.io/component-helpers v0.23.5/go.mod h1:5riXJgjTIs+ZB8xnf5M2anZ8iQuq37a0B/0BgoPQuSM= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kubectl v0.23.5 h1:DmDULqCaF4qstj0Im143XmncvqWtJxHzK8IrW2BzlU0= +k8s.io/kubectl v0.23.5/go.mod h1:lLgw7cVY8xbd7o637vOXPca/w6HC205KsPCRDYRCxwE= +k8s.io/metrics v0.23.5/go.mod h1:WNAtV2a5BYbmDS8+7jSqYYV6E3efuGTpIwJ8PTD1wgs= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= +k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= @@ -951,10 +1014,15 @@ sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0Cwgq sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= +sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= +sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= +sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= +sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/structured-merge-diff/v4 v4.2.0 h1:kDvPBbnPk+qYmkHmSo8vKGp438IASWofnbbUKDE/bv0= sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= +sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/kubernetessecrets/kubernetes_secrets_builder.go b/pkg/kubernetessecrets/kubernetes_secrets_builder.go new file mode 100644 index 0000000..2cd3a05 --- /dev/null +++ b/pkg/kubernetessecrets/kubernetes_secrets_builder.go @@ -0,0 +1,185 @@ +package kubernetessecrets + +import ( + "context" + "fmt" + + "regexp" + "strings" + + "reflect" + + errs "errors" + + "github.com/1Password/connect-sdk-go/onepassword" + + "github.com/1Password/onepassword-operator/pkg/utils" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + kubeValidate "k8s.io/apimachinery/pkg/util/validation" + + kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +const OnepasswordPrefix = "operator.1password.io" +const NameAnnotation = OnepasswordPrefix + "/item-name" +const VersionAnnotation = OnepasswordPrefix + "/item-version" +const restartAnnotation = OnepasswordPrefix + "/last-restarted" +const ItemPathAnnotation = OnepasswordPrefix + "/item-path" +const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" + +var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable") + +var log = logf.Log + +func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string, ownerRef *metav1.OwnerReference) error { + + itemVersion := fmt.Sprint(item.Version) + + // If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map + if secretAnnotations == nil { + secretAnnotations = map[string]string{} + } + + secretAnnotations[VersionAnnotation] = itemVersion + secretAnnotations[ItemPathAnnotation] = fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) + + if autoRestart != "" { + _, err := utils.StringToBool(autoRestart) + if err != nil { + log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) + return err + } + secretAnnotations[RestartDeploymentsAnnotation] = autoRestart + } + + // "Opaque" and "" secret types are treated the same by Kubernetes. + secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item, ownerRef) + + currentSecret := &corev1.Secret{} + err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) + if err != nil && errors.IsNotFound(err) { + log.Info(fmt.Sprintf("Creating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) + return kubeClient.Create(context.Background(), secret) + } else if err != nil { + return err + } + + currentAnnotations := currentSecret.Annotations + currentLabels := currentSecret.Labels + currentSecretType := string(currentSecret.Type) + if !reflect.DeepEqual(currentSecretType, secretType) { + return ErrCannotUpdateSecretType + } + + if !reflect.DeepEqual(currentAnnotations, secretAnnotations) || !reflect.DeepEqual(currentLabels, labels) { + log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) + currentSecret.ObjectMeta.Annotations = secretAnnotations + currentSecret.ObjectMeta.Labels = labels + currentSecret.Data = secret.Data + return kubeClient.Update(context.Background(), currentSecret) + } + + log.Info(fmt.Sprintf("Secret with name %v and version %v already exists", secret.Name, secret.Annotations[VersionAnnotation])) + return nil +} + +func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item, ownerRef *metav1.OwnerReference) *corev1.Secret { + var ownerRefs []metav1.OwnerReference + if ownerRef != nil { + ownerRefs = []metav1.OwnerReference{*ownerRef} + } + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: formatSecretName(name), + Namespace: namespace, + Annotations: annotations, + Labels: labels, + OwnerReferences: ownerRefs, + }, + Data: BuildKubernetesSecretData(item.Fields, item.Files), + Type: corev1.SecretType(secretType), + } +} + +func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte { + secretData := map[string][]byte{} + for i := 0; i < len(fields); i++ { + if fields[i].Value != "" { + key := formatSecretDataName(fields[i].Label) + secretData[key] = []byte(fields[i].Value) + } + } + + // populate unpopulated fields from files + for _, file := range files { + content, err := file.Content() + if err != nil { + log.Error(err, "Could not load contents of file %s", file.Name) + continue + } + if content != nil { + key := file.Name + if secretData[key] == nil { + secretData[key] = content + } else { + log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name)) + } + } + } + return secretData +} + +// formatSecretName rewrites a value to be a valid Secret name. +// +// The Secret meta.name and data keys must be valid DNS subdomain names +// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) +func formatSecretName(value string) string { + if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 { + return value + } + return createValidSecretName(value) +} + +// formatSecretDataName rewrites a value to be a valid Secret data key. +// +// The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.` +// (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) +func formatSecretDataName(value string) string { + if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 { + return value + } + return createValidSecretDataName(value) +} + +var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+") + +func createValidSecretName(value string) string { + result := strings.ToLower(value) + result = invalidDNS1123Chars.ReplaceAllString(result, "-") + + if len(result) > kubeValidate.DNS1123SubdomainMaxLength { + result = result[0:kubeValidate.DNS1123SubdomainMaxLength] + } + + // first and last character MUST be alphanumeric + return strings.Trim(result, "-.") +} + +var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+") +var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)") + +func createValidSecretDataName(value string) string { + result := invalidStartEndChars.ReplaceAllString(value, "") + result = invalidDataChars.ReplaceAllString(result, "-") + + if len(result) > kubeValidate.DNS1123SubdomainMaxLength { + result = result[0:kubeValidate.DNS1123SubdomainMaxLength] + } + + return result +} diff --git a/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go b/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go new file mode 100644 index 0000000..4747504 --- /dev/null +++ b/pkg/kubernetessecrets/kubernetes_secrets_builder_test.go @@ -0,0 +1,313 @@ +package kubernetessecrets + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/1Password/connect-sdk-go/onepassword" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + kubeValidate "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const restartDeploymentAnnotation = "false" + +type k8s struct { + clientset kubernetes.Interface +} + +func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { + secretName := "test-secret-name" + namespace := "test" + + item := onepassword.Item{} + item.Fields = generateFields(5) + item.Version = 123 + item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.ID = "h46bb3jddvay7nxopfhvlwg35q" + + kubeClient := fake.NewFakeClient() + secretLabels := map[string]string{} + secretAnnotations := map[string]string{ + "testAnnotation": "exists", + } + secretType := "" + + err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + createdSecret := &corev1.Secret{} + err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) + + if err != nil { + t.Errorf("Secret was not created: %v", err) + } + compareFields(item.Fields, createdSecret.Data, t) + compareAnnotationsToItem(createdSecret.Annotations, item, t) + + if createdSecret.Annotations["testAnnotation"] != "exists" { + t.Errorf("Expected testAnnotation to be merged with existing annotations, but wasn't.") + } +} + +func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) { + secretName := "test-secret-name" + namespace := "test" + + item := onepassword.Item{} + item.Fields = generateFields(5) + item.Version = 123 + item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.ID = "h46bb3jddvay7nxopfhvlwg35q" + + kubeClient := fake.NewFakeClient() + secretLabels := map[string]string{} + secretAnnotations := map[string]string{ + "testAnnotation": "exists", + } + secretType := "" + + ownerRef := &metav1.OwnerReference{ + Kind: "Deployment", + APIVersion: "apps/v1", + Name: "test-deployment", + UID: types.UID("test-uid"), + } + err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, ownerRef) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + createdSecret := &corev1.Secret{} + err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) + + // Check owner references. + gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences + if len(gotOwnerRefs) != 1 { + t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs)) + } + + expOwnerRef := metav1.OwnerReference{ + Kind: "Deployment", + APIVersion: "apps/v1", + Name: "test-deployment", + UID: types.UID("test-uid"), + } + gotOwnerRef := gotOwnerRefs[0] + if gotOwnerRef != expOwnerRef { + t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef) + } +} + +func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { + secretName := "test-secret-update" + namespace := "test" + + item := onepassword.Item{} + item.Fields = generateFields(5) + item.Version = 123 + item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.ID = "h46bb3jddvay7nxopfhvlwg35q" + + kubeClient := fake.NewFakeClient() + secretLabels := map[string]string{} + secretAnnotations := map[string]string{} + secretType := "" + + err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + // Updating kubernetes secret with new item + newItem := onepassword.Item{} + newItem.Fields = generateFields(6) + newItem.Version = 456 + newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" + err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + updatedSecret := &corev1.Secret{} + err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, updatedSecret) + + if err != nil { + t.Errorf("Secret was not found: %v", err) + } + compareFields(newItem.Fields, updatedSecret.Data, t) + compareAnnotationsToItem(updatedSecret.Annotations, newItem, t) +} +func TestBuildKubernetesSecretData(t *testing.T) { + fields := generateFields(5) + + secretData := BuildKubernetesSecretData(fields, nil) + if len(secretData) != len(fields) { + t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData)) + } + compareFields(fields, secretData, t) +} + +func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { + annotationKey := "annotationKey" + annotationValue := "annotationValue" + name := "someName" + namespace := "someNamespace" + annotations := map[string]string{ + annotationKey: annotationValue, + } + item := onepassword.Item{} + item.Fields = generateFields(5) + labels := map[string]string{} + secretType := "" + + kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil) + if kubeSecret.Name != strings.ToLower(name) { + t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) + } + if kubeSecret.Namespace != namespace { + t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) + } + if kubeSecret.Annotations[annotationKey] != annotations[annotationKey] { + t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) + } + compareFields(item.Fields, kubeSecret.Data, t) +} + +func TestBuildKubernetesSecretFixesInvalidLabels(t *testing.T) { + name := "inV@l1d k8s secret%name" + expectedName := "inv-l1d-k8s-secret-name" + namespace := "someNamespace" + annotations := map[string]string{ + "annotationKey": "annotationValue", + } + labels := map[string]string{} + item := onepassword.Item{} + secretType := "" + + item.Fields = []*onepassword.ItemField{ + { + Label: "label w%th invalid ch!rs-", + Value: "value1", + }, + { + Label: strings.Repeat("x", kubeValidate.DNS1123SubdomainMaxLength+1), + Value: "name exceeds max length", + }, + } + + kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil) + + // Assert Secret's meta.name was fixed + if kubeSecret.Name != expectedName { + t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) + } + if kubeSecret.Namespace != namespace { + t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) + } + + // assert labels were fixed for each data key + for key := range kubeSecret.Data { + if !validLabel(key) { + t.Errorf("Expected valid kubernetes label, got %s", key) + } + } +} + +func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) { + secretName := "tls-test-secret-name" + namespace := "test" + + item := onepassword.Item{} + item.Fields = generateFields(5) + item.Version = 123 + item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" + item.ID = "h46bb3jddvay7nxopfhvlwg35q" + + kubeClient := fake.NewFakeClient() + secretLabels := map[string]string{} + secretAnnotations := map[string]string{ + "testAnnotation": "exists", + } + secretType := "kubernetes.io/tls" + + err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations, nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + createdSecret := &corev1.Secret{} + err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) + + if err != nil { + t.Errorf("Secret was not created: %v", err) + } + + if createdSecret.Type != corev1.SecretTypeTLS { + t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type)) + } +} + +func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { + actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) + if err != nil { + t.Errorf("Was unable to parse Item Path") + } + if actualVaultId != item.Vault.ID { + t.Errorf("Expected annotation vault id to be %v but was %v", item.Vault.ID, actualVaultId) + } + if actualItemId != item.ID { + t.Errorf("Expected annotation item id to be %v but was %v", item.ID, actualItemId) + } + if annotations[VersionAnnotation] != fmt.Sprint(item.Version) { + t.Errorf("Expected annotation version to be %v but was %v", item.Version, annotations[VersionAnnotation]) + } + + if annotations[RestartDeploymentsAnnotation] != "false" { + t.Errorf("Expected restart deployments annotation to be %v but was %v", restartDeploymentAnnotation, RestartDeploymentsAnnotation) + } +} + +func compareFields(actualFields []*onepassword.ItemField, secretData map[string][]byte, t *testing.T) { + for i := 0; i < len(actualFields); i++ { + value, found := secretData[actualFields[i].Label] + if !found { + t.Errorf("Expected key %v is missing from secret data", actualFields[i].Label) + } + if string(value) != actualFields[i].Value { + t.Errorf("Expected value %v but got %v", actualFields[i].Value, value) + } + } +} + +func generateFields(numToGenerate int) []*onepassword.ItemField { + fields := []*onepassword.ItemField{} + for i := 0; i < numToGenerate; i++ { + field := onepassword.ItemField{ + Label: "key" + fmt.Sprint(i), + Value: "value" + fmt.Sprint(i), + } + fields = append(fields, &field) + } + return fields +} + +func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) { + splitPath := strings.Split(path, "/") + if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { + return splitPath[1], splitPath[3], nil + } + return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) +} + +func validLabel(v string) bool { + if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 { + return false + } + return true +} diff --git a/pkg/mocks/mocksecretserver.go b/pkg/mocks/mocksecretserver.go new file mode 100644 index 0000000..29c7664 --- /dev/null +++ b/pkg/mocks/mocksecretserver.go @@ -0,0 +1,84 @@ +package mocks + +import ( + "github.com/1Password/connect-sdk-go/onepassword" +) + +type TestClient struct { + GetVaultsFunc func() ([]onepassword.Vault, error) + GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) + GetVaultFunc func(uuid string) (*onepassword.Vault, error) + GetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error) + GetItemsFunc func(vaultUUID string) ([]onepassword.Item, error) + GetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error) + GetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) + CreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) + UpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) + DeleteItemFunc func(item *onepassword.Item, vaultUUID string) error + GetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) + GetFileContentFunc func(file *onepassword.File) ([]byte, error) +} + +var ( + GetGetVaultsFunc func() ([]onepassword.Vault, error) + DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) + DoGetVaultFunc func(uuid string) (*onepassword.Vault, error) + GetGetItemFunc func(uuid string, vaultUUID string) (*onepassword.Item, error) + DoGetItemsByTitleFunc func(title string, vaultUUID string) ([]onepassword.Item, error) + DoGetItemByTitleFunc func(title string, vaultUUID string) (*onepassword.Item, error) + DoCreateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) + DoDeleteItemFunc func(item *onepassword.Item, vaultUUID string) error + DoGetItemsFunc func(vaultUUID string) ([]onepassword.Item, error) + DoUpdateItemFunc func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) + DoGetFileFunc func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) + DoGetFileContentFunc func(file *onepassword.File) ([]byte, error) +) + +// Do is the mock client's `Do` func +func (m *TestClient) GetVaults() ([]onepassword.Vault, error) { + return GetGetVaultsFunc() +} + +func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { + return DoGetVaultsByTitleFunc(title) +} + +func (m *TestClient) GetVault(uuid string) (*onepassword.Vault, error) { + return DoGetVaultFunc(uuid) +} + +func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) { + return GetGetItemFunc(uuid, vaultUUID) +} + +func (m *TestClient) GetItems(vaultUUID string) ([]onepassword.Item, error) { + return DoGetItemsFunc(vaultUUID) +} + +func (m *TestClient) GetItemsByTitle(title, vaultUUID string) ([]onepassword.Item, error) { + return DoGetItemsByTitleFunc(title, vaultUUID) +} + +func (m *TestClient) GetItemByTitle(title string, vaultUUID string) (*onepassword.Item, error) { + return DoGetItemByTitleFunc(title, vaultUUID) +} + +func (m *TestClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { + return DoCreateItemFunc(item, vaultUUID) +} + +func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error { + return DoDeleteItemFunc(item, vaultUUID) +} + +func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { + return DoUpdateItemFunc(item, vaultUUID) +} + +func (m *TestClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) { + return DoGetFileFunc(uuid, itemUUID, vaultUUID) +} + +func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) { + return DoGetFileContentFunc(file) +} diff --git a/pkg/onepassword/annotations.go b/pkg/onepassword/annotations.go new file mode 100644 index 0000000..652f671 --- /dev/null +++ b/pkg/onepassword/annotations.go @@ -0,0 +1,60 @@ +package onepassword + +import ( + "regexp" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +const ( + OnepasswordPrefix = "operator.1password.io" + ItemPathAnnotation = OnepasswordPrefix + "/item-path" + NameAnnotation = OnepasswordPrefix + "/item-name" + VersionAnnotation = OnepasswordPrefix + "/item-version" + RestartAnnotation = OnepasswordPrefix + "/last-restarted" + RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" +) + +func GetAnnotationsForDeployment(deployment *appsv1.Deployment, regex *regexp.Regexp) (map[string]string, bool) { + annotationsFound := false + annotations := FilterAnnotations(deployment.Annotations, regex) + if len(annotations) > 0 { + annotationsFound = true + } else { + annotations = FilterAnnotations(deployment.Spec.Template.Annotations, regex) + if len(annotations) > 0 { + annotationsFound = true + } else { + annotationsFound = false + } + } + + return annotations, annotationsFound +} + +func FilterAnnotations(annotations map[string]string, regex *regexp.Regexp) map[string]string { + filteredAnnotations := make(map[string]string) + for key, value := range annotations { + if regex.MatchString(key) && key != RestartAnnotation && key != RestartDeploymentsAnnotation { + filteredAnnotations[key] = value + } + } + return filteredAnnotations +} + +func AreAnnotationsUsingSecrets(annotations map[string]string, secrets map[string]*corev1.Secret) bool { + _, ok := secrets[annotations[NameAnnotation]] + if ok { + return true + } + return false +} + +func AppendAnnotationUpdatedSecret(annotations map[string]string, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { + secret, ok := secrets[annotations[NameAnnotation]] + if ok { + updatedDeploymentSecrets[secret.Name] = secret + } + return updatedDeploymentSecrets +} diff --git a/pkg/onepassword/annotations_test.go b/pkg/onepassword/annotations_test.go new file mode 100644 index 0000000..3949f78 --- /dev/null +++ b/pkg/onepassword/annotations_test.go @@ -0,0 +1,93 @@ +package onepassword + +import ( + "regexp" + "testing" + + appsv1 "k8s.io/api/apps/v1" +) + +const AnnotationRegExpString = "^operator.1password.io\\/[a-zA-Z\\.]+" + +func TestFilterAnnotations(t *testing.T) { + invalidAnnotation1 := "onepasswordconnect/vaultId" + invalidAnnotation2 := "onepasswordconnectkubernetesSecrets" + + annotations := getValidAnnotations() + annotations[invalidAnnotation1] = "This should be filtered" + annotations[invalidAnnotation2] = "This should be filtered too" + + r, _ := regexp.Compile(AnnotationRegExpString) + filteredAnnotations := FilterAnnotations(annotations, r) + if len(filteredAnnotations) != 2 { + t.Errorf("Unexpected number of filtered annotations returned. Expected 2, got %v", len(filteredAnnotations)) + } + _, found := filteredAnnotations[ItemPathAnnotation] + if !found { + t.Errorf("One Password Annotation was filtered when it should not have been") + } + _, found = filteredAnnotations[NameAnnotation] + if !found { + t.Errorf("One Password Annotation was filtered when it should not have been") + } +} + +func TestGetTopLevelAnnotationsForDeployment(t *testing.T) { + annotations := getValidAnnotations() + expectedNumAnnotations := len(annotations) + r, _ := regexp.Compile(AnnotationRegExpString) + + deployment := &appsv1.Deployment{} + deployment.Annotations = annotations + filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r) + + if !annotationsFound { + t.Errorf("No annotations marked as found") + } + + numAnnotations := len(filteredAnnotations) + if expectedNumAnnotations != numAnnotations { + t.Errorf("Expected %v annotations got %v", expectedNumAnnotations, numAnnotations) + } +} + +func TestGetTemplateAnnotationsForDeployment(t *testing.T) { + annotations := getValidAnnotations() + expectedNumAnnotations := len(annotations) + r, _ := regexp.Compile(AnnotationRegExpString) + + deployment := &appsv1.Deployment{} + deployment.Spec.Template.Annotations = annotations + filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r) + + if !annotationsFound { + t.Errorf("No annotations marked as found") + } + + numAnnotations := len(filteredAnnotations) + if expectedNumAnnotations != numAnnotations { + t.Errorf("Expected %v annotations got %v", expectedNumAnnotations, numAnnotations) + } +} + +func TestGetNoAnnotationsForDeployment(t *testing.T) { + deployment := &appsv1.Deployment{} + r, _ := regexp.Compile(AnnotationRegExpString) + filteredAnnotations, annotationsFound := GetAnnotationsForDeployment(deployment, r) + + if annotationsFound { + t.Errorf("No annotations should be found") + } + + numAnnotations := len(filteredAnnotations) + if 0 != numAnnotations { + t.Errorf("Expected %v annotations got %v", 0, numAnnotations) + } +} + +func getValidAnnotations() map[string]string { + return map[string]string{ + ItemPathAnnotation: "vaults/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f/items/b3e4c7fc-8bf7-4c22-b8bb-147539f10e4f", + NameAnnotation: "secretName", + } +} diff --git a/pkg/onepassword/connect_setup.go b/pkg/onepassword/connect_setup.go new file mode 100644 index 0000000..ade8caa --- /dev/null +++ b/pkg/onepassword/connect_setup.go @@ -0,0 +1,117 @@ +package onepassword + +import ( + "context" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "os" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +var logConnectSetup = logf.Log.WithName("ConnectSetup") +var deploymentPath = "deploy/connect/deployment.yaml" +var servicePath = "deploy/connect/service.yaml" + +func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { + err := setupService(kubeClient, servicePath, deploymentNamespace) + if err != nil { + return err + } + + err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace) + if err != nil { + return err + } + + return nil +} + +func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { + existingDeployment := &appsv1.Deployment{} + + // check if deployment has already been created + err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment) + if err != nil { + if errors.IsNotFound(err) { + logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") + return createDeployment(kubeClient, deploymentPath, deploymentNamespace) + } + } + return err +} + +func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { + deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace) + if err != nil { + return err + } + + err = kubeClient.Create(context.Background(), deployment) + if err != nil { + return err + } + + return nil +} + +func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) { + f, err := os.Open(deploymentPath) + if err != nil { + return nil, err + } + deployment := &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Namespace: deploymentNamespace, + }, + } + + err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) + if err != nil { + return nil, err + } + return deployment, nil +} + +func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { + existingService := &corev1.Service{} + + //check if service has already been created + err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService) + if err != nil { + if errors.IsNotFound(err) { + logConnectSetup.Info("No existing Connect service found. Creating Service") + return createService(kubeClient, servicePath, deploymentNamespace) + } + } + return err +} + +func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { + f, err := os.Open(servicePath) + if err != nil { + return err + } + service := &corev1.Service{ + ObjectMeta: v1.ObjectMeta{ + Namespace: deploymentNamespace, + }, + } + + err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) + if err != nil { + return err + } + + err = kubeClient.Create(context.Background(), service) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/onepassword/connect_setup_test.go b/pkg/onepassword/connect_setup_test.go new file mode 100644 index 0000000..03e9daa --- /dev/null +++ b/pkg/onepassword/connect_setup_test.go @@ -0,0 +1,65 @@ +package onepassword + +import ( + "context" + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubectl/pkg/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var defaultNamespacedName = types.NamespacedName{Name: "onepassword-connect", Namespace: "default"} + +func TestServiceSetup(t *testing.T) { + + // Register operator types with the runtime scheme. + s := scheme.Scheme + + // Objects to track in the fake client. + objs := []runtime.Object{} + + // Create a fake client to mock API calls. + client := fake.NewFakeClientWithScheme(s, objs...) + + err := setupService(client, "../../deploy/connect/service.yaml", defaultNamespacedName.Namespace) + + if err != nil { + t.Errorf("Error Setting Up Connect: %v", err) + } + + // check that service was created + service := &corev1.Service{} + err = client.Get(context.TODO(), defaultNamespacedName, service) + if err != nil { + t.Errorf("Error Setting Up Connect service: %v", err) + } +} + +func TestDeploymentSetup(t *testing.T) { + + // Register operator types with the runtime scheme. + s := scheme.Scheme + + // Objects to track in the fake client. + objs := []runtime.Object{} + + // Create a fake client to mock API calls. + client := fake.NewFakeClientWithScheme(s, objs...) + + err := setupDeployment(client, "../../deploy/connect/deployment.yaml", defaultNamespacedName.Namespace) + + if err != nil { + t.Errorf("Error Setting Up Connect: %v", err) + } + + // check that deployment was created + deployment := &appsv1.Deployment{} + err = client.Get(context.TODO(), defaultNamespacedName, deployment) + if err != nil { + t.Errorf("Error Setting Up Connect deployment: %v", err) + } +} diff --git a/pkg/onepassword/containers.go b/pkg/onepassword/containers.go new file mode 100644 index 0000000..c0910a8 --- /dev/null +++ b/pkg/onepassword/containers.go @@ -0,0 +1,53 @@ +package onepassword + +import ( + corev1 "k8s.io/api/core/v1" +) + +func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool { + for i := 0; i < len(containers); i++ { + envVariables := containers[i].Env + for j := 0; j < len(envVariables); j++ { + if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil { + _, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name] + if ok { + return true + } + } + } + envFromVariables := containers[i].EnvFrom + for j := 0; j < len(envFromVariables); j++ { + if envFromVariables[j].SecretRef != nil { + _, ok := secrets[envFromVariables[j].SecretRef.Name] + if ok { + return true + } + } + } + } + return false +} + +func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { + for i := 0; i < len(containers); i++ { + envVariables := containers[i].Env + for j := 0; j < len(envVariables); j++ { + if envVariables[j].ValueFrom != nil && envVariables[j].ValueFrom.SecretKeyRef != nil { + secret, ok := secrets[envVariables[j].ValueFrom.SecretKeyRef.Name] + if ok { + updatedDeploymentSecrets[secret.Name] = secret + } + } + } + envFromVariables := containers[i].EnvFrom + for j := 0; j < len(envFromVariables); j++ { + if envFromVariables[j].SecretRef != nil { + secret, ok := secrets[envFromVariables[j].SecretRef.LocalObjectReference.Name] + if ok { + updatedDeploymentSecrets[secret.Name] = secret + } + } + } + } + return updatedDeploymentSecrets +} diff --git a/pkg/onepassword/containers_test.go b/pkg/onepassword/containers_test.go new file mode 100644 index 0000000..c6540f9 --- /dev/null +++ b/pkg/onepassword/containers_test.go @@ -0,0 +1,85 @@ +package onepassword + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAreContainersUsingSecretsFromEnv(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": &corev1.Secret{}, + "onepassword-api-key": &corev1.Secret{}, + } + + containerSecretNames := []string{ + "onepassword-database-secret", + "onepassword-api-key", + "some_other_key", + } + + containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) + + if !AreContainersUsingSecrets(containers, secretNamesToSearch) { + t.Errorf("Expected that containers were using secrets but they were not detected.") + } +} + +func TestAreContainersUsingSecretsFromEnvFrom(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": {}, + "onepassword-api-key": {}, + } + + containerSecretNames := []string{ + "onepassword-database-secret", + "onepassword-api-key", + "some_other_key", + } + + containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) + + if !AreContainersUsingSecrets(containers, secretNamesToSearch) { + t.Errorf("Expected that containers were using secrets but they were not detected.") + } +} + +func TestAreContainersNotUsingSecrets(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": {}, + "onepassword-api-key": {}, + } + + containerSecretNames := []string{ + "some_other_key", + } + + containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) + + if AreContainersUsingSecrets(containers, secretNamesToSearch) { + t.Errorf("Expected that containers were not using secrets but they were detected.") + } +} + +func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": {}, + "onepassword-api-key": {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}}, + } + + containerSecretNames := []string{ + "onepassword-api-key", + } + + containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) + + updatedDeploymentSecrets := map[string]*corev1.Secret{} + updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets) + + secretKeyName := "onepassword-api-key" + + if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] { + t.Errorf("Expected that updated Secret from envfrom is found.") + } +} diff --git a/pkg/onepassword/deployments.go b/pkg/onepassword/deployments.go new file mode 100644 index 0000000..6c97efb --- /dev/null +++ b/pkg/onepassword/deployments.go @@ -0,0 +1,26 @@ +package onepassword + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +func IsDeploymentUsingSecrets(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) bool { + volumes := deployment.Spec.Template.Spec.Volumes + containers := deployment.Spec.Template.Spec.Containers + containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) + return AreAnnotationsUsingSecrets(deployment.Annotations, secrets) || AreContainersUsingSecrets(containers, secrets) || AreVolumesUsingSecrets(volumes, secrets) +} + +func GetUpdatedSecretsForDeployment(deployment *appsv1.Deployment, secrets map[string]*corev1.Secret) map[string]*corev1.Secret { + volumes := deployment.Spec.Template.Spec.Volumes + containers := deployment.Spec.Template.Spec.Containers + containers = append(containers, deployment.Spec.Template.Spec.InitContainers...) + + updatedSecretsForDeployment := map[string]*corev1.Secret{} + AppendAnnotationUpdatedSecret(deployment.Annotations, secrets, updatedSecretsForDeployment) + AppendUpdatedContainerSecrets(containers, secrets, updatedSecretsForDeployment) + AppendUpdatedVolumeSecrets(volumes, secrets, updatedSecretsForDeployment) + + return updatedSecretsForDeployment +} diff --git a/pkg/onepassword/deployments_test.go b/pkg/onepassword/deployments_test.go new file mode 100644 index 0000000..45761fc --- /dev/null +++ b/pkg/onepassword/deployments_test.go @@ -0,0 +1,58 @@ +package onepassword + +import ( + "testing" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +func TestIsDeploymentUsingSecretsUsingVolumes(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": &corev1.Secret{}, + "onepassword-api-key": &corev1.Secret{}, + } + + volumeSecretNames := []string{ + "onepassword-database-secret", + "onepassword-api-key", + "some_other_key", + } + + deployment := &appsv1.Deployment{} + deployment.Spec.Template.Spec.Volumes = generateVolumes(volumeSecretNames) + if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { + t.Errorf("Expected that deployment was using secrets but they were not detected.") + } +} + +func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": &corev1.Secret{}, + "onepassword-api-key": &corev1.Secret{}, + } + + containerSecretNames := []string{ + "onepassword-database-secret", + "onepassword-api-key", + "some_other_key", + } + + deployment := &appsv1.Deployment{} + deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames) + if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { + t.Errorf("Expected that deployment was using secrets but they were not detected.") + } +} + +func TestIsDeploymentNotUSingSecrets(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": &corev1.Secret{}, + "onepassword-api-key": &corev1.Secret{}, + } + + deployment := &appsv1.Deployment{} + if IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { + t.Errorf("Expected that deployment was using not secrets but they were detected.") + } +} diff --git a/pkg/onepassword/items.go b/pkg/onepassword/items.go new file mode 100644 index 0000000..c022f5d --- /dev/null +++ b/pkg/onepassword/items.go @@ -0,0 +1,100 @@ +package onepassword + +import ( + "fmt" + "strings" + + "github.com/1Password/connect-sdk-go/connect" + "github.com/1Password/connect-sdk-go/onepassword" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +var logger = logf.Log.WithName("retrieve_item") + +func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*onepassword.Item, error) { + vaultValue, itemValue, err := ParseVaultAndItemFromPath(path) + if err != nil { + return nil, err + } + vaultId, err := getVaultId(opConnectClient, vaultValue) + if err != nil { + return nil, err + } + + itemId, err := getItemId(opConnectClient, itemValue, vaultId) + if err != nil { + return nil, err + } + + item, err := opConnectClient.GetItem(itemId, vaultId) + if err != nil { + return nil, err + } + + for _, file := range item.Files { + _, err := opConnectClient.GetFileContent(file) + if err != nil { + return nil, err + } + } + + return item, nil +} + +func ParseVaultAndItemFromPath(path string) (string, string, error) { + splitPath := strings.Split(path, "/") + if len(splitPath) == 4 && splitPath[0] == "vaults" && splitPath[2] == "items" { + return splitPath[1], splitPath[3], nil + } + return "", "", fmt.Errorf("%q is not an acceptable path for One Password item. Must be of the format: `vaults/{vault_id}/items/{item_id}`", path) +} + +func getVaultId(client connect.Client, vaultIdentifier string) (string, error) { + if !IsValidClientUUID(vaultIdentifier) { + vaults, err := client.GetVaultsByTitle(vaultIdentifier) + if err != nil { + return "", err + } + + if len(vaults) == 0 { + return "", fmt.Errorf("No vaults found with identifier %q", vaultIdentifier) + } + + oldestVault := vaults[0] + if len(vaults) > 1 { + for _, returnedVault := range vaults { + if returnedVault.CreatedAt.Before(oldestVault.CreatedAt) { + oldestVault = returnedVault + } + } + logger.Info(fmt.Sprintf("%v 1Password vaults found with the title %q. Will use vault %q as it is the oldest.", len(vaults), vaultIdentifier, oldestVault.ID)) + } + vaultIdentifier = oldestVault.ID + } + return vaultIdentifier, nil +} + +func getItemId(client connect.Client, itemIdentifier string, vaultId string) (string, error) { + if !IsValidClientUUID(itemIdentifier) { + items, err := client.GetItemsByTitle(itemIdentifier, vaultId) + if err != nil { + return "", err + } + + if len(items) == 0 { + return "", fmt.Errorf("No items found with identifier %q", itemIdentifier) + } + + oldestItem := items[0] + if len(items) > 1 { + for _, returnedItem := range items { + if returnedItem.CreatedAt.Before(oldestItem.CreatedAt) { + oldestItem = returnedItem + } + } + logger.Info(fmt.Sprintf("%v 1Password items found with the title %q. Will use item %q as it is the oldest.", len(items), itemIdentifier, oldestItem.ID)) + } + itemIdentifier = oldestItem.ID + } + return itemIdentifier, nil +} diff --git a/pkg/onepassword/object_generators_for_test.go b/pkg/onepassword/object_generators_for_test.go new file mode 100644 index 0000000..2392070 --- /dev/null +++ b/pkg/onepassword/object_generators_for_test.go @@ -0,0 +1,54 @@ +package onepassword + +import corev1 "k8s.io/api/core/v1" + +func generateVolumes(names []string) []corev1.Volume { + volumes := []corev1.Volume{} + for i := 0; i < len(names); i++ { + volume := corev1.Volume{ + Name: names[i], + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: names[i], + }, + }, + } + volumes = append(volumes, volume) + } + return volumes +} +func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container { + containers := []corev1.Container{} + for i := 0; i < len(names); i++ { + container := corev1.Container{ + Env: []corev1.EnvVar{ + { + Name: "someName", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: names[i], + }, + Key: "password", + }, + }, + }, + }, + } + containers = append(containers, container) + } + return containers +} + +func generateContainersWithSecretRefsFromEnvFrom(names []string) []corev1.Container { + containers := []corev1.Container{} + for i := 0; i < len(names); i++ { + container := corev1.Container{ + EnvFrom: []corev1.EnvFromSource{ + {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: names[i]}}}, + }, + } + containers = append(containers, container) + } + return containers +} diff --git a/pkg/onepassword/secret_update_handler.go b/pkg/onepassword/secret_update_handler.go new file mode 100644 index 0000000..0418626 --- /dev/null +++ b/pkg/onepassword/secret_update_handler.go @@ -0,0 +1,248 @@ +package onepassword + +import ( + "context" + "fmt" + "time" + + v1 "github.com/1Password/onepassword-operator/api/v1" + + kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" + "github.com/1Password/onepassword-operator/pkg/utils" + + "github.com/1Password/connect-sdk-go/connect" + "github.com/1Password/connect-sdk-go/onepassword" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +const envHostVariable = "OP_HOST" +const lockTag = "operator.1password.io:ignore-secret" + +var log = logf.Log.WithName("update_op_kubernetes_secrets_task") + +func NewManager(kubernetesClient client.Client, opConnectClient connect.Client, shouldAutoRestartDeploymentsGlobal bool) *SecretUpdateHandler { + return &SecretUpdateHandler{ + client: kubernetesClient, + opConnectClient: opConnectClient, + shouldAutoRestartDeploymentsGlobal: shouldAutoRestartDeploymentsGlobal, + } +} + +type SecretUpdateHandler struct { + client client.Client + opConnectClient connect.Client + shouldAutoRestartDeploymentsGlobal bool +} + +func (h *SecretUpdateHandler) UpdateKubernetesSecretsTask() error { + updatedKubernetesSecrets, err := h.updateKubernetesSecrets() + if err != nil { + return err + } + + return h.restartDeploymentsWithUpdatedSecrets(updatedKubernetesSecrets) +} + +func (h *SecretUpdateHandler) restartDeploymentsWithUpdatedSecrets(updatedSecretsByNamespace map[string]map[string]*corev1.Secret) error { + // No secrets to update. Exit + if len(updatedSecretsByNamespace) == 0 || updatedSecretsByNamespace == nil { + return nil + } + + deployments := &appsv1.DeploymentList{} + err := h.client.List(context.Background(), deployments) + if err != nil { + log.Error(err, "Failed to list kubernetes deployments") + return err + } + + if len(deployments.Items) == 0 { + return nil + } + + setForAutoRestartByNamespaceMap, err := h.getIsSetForAutoRestartByNamespaceMap() + if err != nil { + return err + } + + for i := 0; i < len(deployments.Items); i++ { + deployment := &deployments.Items[i] + updatedSecrets := updatedSecretsByNamespace[deployment.Namespace] + + updatedDeploymentSecrets := GetUpdatedSecretsForDeployment(deployment, updatedSecrets) + if len(updatedDeploymentSecrets) == 0 { + continue + } + for _, secret := range updatedDeploymentSecrets { + if isSecretSetForAutoRestart(secret, deployment, setForAutoRestartByNamespaceMap) { + h.restartDeployment(deployment) + continue + } + } + + log.Info(fmt.Sprintf("Deployment %q at namespace %q is up to date", deployment.GetName(), deployment.Namespace)) + + } + return nil +} + +func (h *SecretUpdateHandler) restartDeployment(deployment *appsv1.Deployment) { + log.Info(fmt.Sprintf("Deployment %q at namespace %q references an updated secret. Restarting", deployment.GetName(), deployment.Namespace)) + deployment.Spec.Template.Annotations = map[string]string{ + RestartAnnotation: time.Now().String(), + } + err := h.client.Update(context.Background(), deployment) + if err != nil { + log.Error(err, "Problem restarting deployment") + } +} + +func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]*corev1.Secret, error) { + secrets := &corev1.SecretList{} + err := h.client.List(context.Background(), secrets) + if err != nil { + log.Error(err, "Failed to list kubernetes secrets") + return nil, err + } + + updatedSecrets := map[string]map[string]*corev1.Secret{} + for i := 0; i < len(secrets.Items); i++ { + secret := secrets.Items[i] + + itemPath := secret.Annotations[ItemPathAnnotation] + currentVersion := secret.Annotations[VersionAnnotation] + if len(itemPath) == 0 || len(currentVersion) == 0 { + continue + } + + OnePasswordItemPath := h.getPathFromOnePasswordItem(secret) + + item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath) + if err != nil { + log.Error(err, "failed to retrieve 1Password item at path \"%s\" for secret \"%s\"", secret.Annotations[ItemPathAnnotation], secret.Name) + continue + } + + itemVersion := fmt.Sprint(item.Version) + itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) + + if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString { + if isItemLockedForForcedRestarts(item) { + log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName())) + secret.Annotations[VersionAnnotation] = itemVersion + secret.Annotations[ItemPathAnnotation] = itemPathString + h.client.Update(context.Background(), &secret) + continue + } + log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) + secret.Annotations[VersionAnnotation] = itemVersion + secret.Annotations[ItemPathAnnotation] = itemPathString + updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, string(secret.Type), *item, nil) + log.Info(fmt.Sprintf("New secret path: %v and version: %v", updatedSecret.Annotations[ItemPathAnnotation], updatedSecret.Annotations[VersionAnnotation])) + h.client.Update(context.Background(), updatedSecret) + if updatedSecrets[secret.Namespace] == nil { + updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) + } + updatedSecrets[secret.Namespace][secret.Name] = &secret + } + } + return updatedSecrets, nil +} + +func isItemLockedForForcedRestarts(item *onepassword.Item) bool { + tags := item.Tags + for i := 0; i < len(tags); i++ { + if tags[i] == lockTag { + return true + } + } + return false +} + +func isUpdatedSecret(secretName string, updatedSecrets map[string]*corev1.Secret) bool { + _, ok := updatedSecrets[secretName] + if ok { + return true + } + return false +} + +func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string]bool, error) { + namespaces := &corev1.NamespaceList{} + err := h.client.List(context.Background(), namespaces) + if err != nil { + log.Error(err, "Failed to list kubernetes namespaces") + return nil, err + } + + namespacesMap := map[string]bool{} + + for _, namespace := range namespaces.Items { + namespacesMap[namespace.Name] = h.isNamespaceSetToAutoRestart(&namespace) + } + return namespacesMap, nil +} + +func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string { + onePasswordItem := &v1.OnePasswordItem{} + + // Search for our original OnePasswordItem if it exists + err := h.client.Get(context.TODO(), client.ObjectKey{ + Namespace: secret.Namespace, + Name: secret.Name}, onePasswordItem) + + if err == nil { + return onePasswordItem.Spec.ItemPath + } + + // If we can't find the OnePassword Item we'll just return the annotation from the secret item. + return secret.Annotations[ItemPathAnnotation] +} + +func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { + restartDeployment := secret.Annotations[RestartDeploymentsAnnotation] + //If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace + if restartDeployment == "" { + return isDeploymentSetForAutoRestart(deployment, setForAutoRestartByNamespace) + } + + restartDeploymentBool, err := utils.StringToBool(restartDeployment) + if err != nil { + log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secret.Name) + return false + } + return restartDeploymentBool +} + +func isDeploymentSetForAutoRestart(deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { + restartDeployment := deployment.Annotations[RestartDeploymentsAnnotation] + //If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace + if restartDeployment == "" { + return setForAutoRestartByNamespace[deployment.Namespace] + } + + restartDeploymentBool, err := utils.StringToBool(restartDeployment) + if err != nil { + log.Error(err, "Error parsing %v annotation on Deployment %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, deployment.Name) + return false + } + return restartDeploymentBool +} + +func (h *SecretUpdateHandler) isNamespaceSetToAutoRestart(namespace *corev1.Namespace) bool { + restartDeployment := namespace.Annotations[RestartDeploymentsAnnotation] + //If annotation for auto restarts for deployment is not set. Check environment variable set on the operator + if restartDeployment == "" { + return h.shouldAutoRestartDeploymentsGlobal + } + + restartDeploymentBool, err := utils.StringToBool(restartDeployment) + if err != nil { + log.Error(err, "Error parsing %v annotation on Namespace %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, namespace.Name) + return false + } + return restartDeploymentBool +} diff --git a/pkg/onepassword/secret_update_handler_test.go b/pkg/onepassword/secret_update_handler_test.go new file mode 100644 index 0000000..269a179 --- /dev/null +++ b/pkg/onepassword/secret_update_handler_test.go @@ -0,0 +1,860 @@ +package onepassword + +import ( + "context" + "fmt" + "testing" + + "github.com/1Password/onepassword-operator/pkg/mocks" + + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + errors2 "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/kubectl/pkg/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +const ( + deploymentKind = "Deployment" + deploymentAPIVersion = "v1" + name = "test-deployment" + namespace = "default" + vaultId = "hfnjvi6aymbsnfc2xeeoheizda" + itemId = "nwrhuano7bcwddcviubpp4mhfq" + username = "test-user" + password = "QmHumKc$mUeEem7caHtbaBaJ" + userKey = "username" + passKey = "password" + itemVersion = 123 +) + +type testUpdateSecretTask struct { + testName string + existingDeployment *appsv1.Deployment + existingNamespace *corev1.Namespace + existingSecret *corev1.Secret + expectedError error + expectedResultSecret *corev1.Secret + expectedEvents []string + opItem map[string]string + expectedRestart bool + globalAutoRestartEnabled bool +} + +var ( + expectedSecretData = map[string][]byte{ + "password": []byte(password), + "username": []byte(username), + } + itemPath = fmt.Sprintf("vaults/%v/items/%v", vaultId, itemId) +) + +var defaultNamespace = &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + }, +} + +var tests = []testUpdateSecretTask{ + { + testName: "Test unrelated deployment is not restarted with an updated secret", + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + NameAnnotation: "unlrelated secret", + ItemPathAnnotation: itemPath, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: false, + globalAutoRestartEnabled: true, + }, + { + testName: "OP item has new version. Secret needs update. Deployment is restarted based on containers", + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: passKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: true, + globalAutoRestartEnabled: true, + }, + { + testName: "OP item has new version. Secret needs update. Deployment is restarted based on annotation", + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + ItemPathAnnotation: itemPath, + NameAnnotation: name, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: true, + globalAutoRestartEnabled: true, + }, + { + testName: "OP item has new version. Secret needs update. Deployment is restarted based on volume", + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: name, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: name, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: true, + globalAutoRestartEnabled: true, + }, + { + testName: "No secrets need update. No deployment is restarted", + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + ItemPathAnnotation: itemPath, + NameAnnotation: name, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: false, + globalAutoRestartEnabled: true, + }, + { + testName: `Deployment is not restarted when no auto restart is set to true for all + deployments and is not overwritten by by a namespace or deployment annotation`, + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: passKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: false, + globalAutoRestartEnabled: false, + }, + { + testName: `Secret autostart true value takes precedence over false deployment value`, + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + RestartDeploymentsAnnotation: "false", + }, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: passKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + RestartDeploymentsAnnotation: "true", + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + RestartDeploymentsAnnotation: "true", + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: true, + globalAutoRestartEnabled: false, + }, + { + testName: `Secret autostart true value takes precedence over false deployment value`, + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + RestartDeploymentsAnnotation: "true", + }, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: passKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + RestartDeploymentsAnnotation: "false", + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + RestartDeploymentsAnnotation: "false", + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: false, + globalAutoRestartEnabled: true, + }, + { + testName: `Deployment autostart true value takes precedence over false global auto restart value`, + existingNamespace: defaultNamespace, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + RestartDeploymentsAnnotation: "true", + }, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: passKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: true, + globalAutoRestartEnabled: false, + }, + { + testName: `Deployment autostart false value takes precedence over false global auto restart value, + and true namespace value.`, + existingNamespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + Annotations: map[string]string{ + RestartDeploymentsAnnotation: "true", + }, + }, + }, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + RestartDeploymentsAnnotation: "false", + }, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: passKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: false, + globalAutoRestartEnabled: false, + }, + { + testName: `Namespace autostart true value takes precedence over false global auto restart value`, + existingNamespace: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespace, + Annotations: map[string]string{ + RestartDeploymentsAnnotation: "true", + }, + }, + }, + existingDeployment: &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: deploymentKind, + APIVersion: deploymentAPIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: name, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + Key: passKey, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + existingSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: "old version", + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + expectedError: nil, + expectedResultSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + VersionAnnotation: fmt.Sprint(itemVersion), + ItemPathAnnotation: itemPath, + }, + }, + Data: expectedSecretData, + }, + opItem: map[string]string{ + userKey: username, + passKey: password, + }, + expectedRestart: true, + globalAutoRestartEnabled: false, + }, +} + +func TestUpdateSecretHandler(t *testing.T) { + for _, testData := range tests { + t.Run(testData.testName, func(t *testing.T) { + + // Register operator types with the runtime scheme. + s := scheme.Scheme + s.AddKnownTypes(appsv1.SchemeGroupVersion, testData.existingDeployment) + + // Objects to track in the fake client. + objs := []runtime.Object{ + testData.existingDeployment, + testData.existingNamespace, + } + + if testData.existingSecret != nil { + objs = append(objs, testData.existingSecret) + } + + // Create a fake client to mock API calls. + cl := fake.NewFakeClientWithScheme(s, objs...) + + opConnectClient := &mocks.TestClient{} + mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { + + item := onepassword.Item{} + item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) + item.Version = itemVersion + item.Vault.ID = vaultUUID + item.ID = uuid + return &item, nil + } + h := &SecretUpdateHandler{ + client: cl, + opConnectClient: opConnectClient, + shouldAutoRestartDeploymentsGlobal: testData.globalAutoRestartEnabled, + } + + err := h.UpdateKubernetesSecretsTask() + + assert.Equal(t, testData.expectedError, err) + + var expectedSecretName string + if testData.expectedResultSecret == nil { + expectedSecretName = testData.existingDeployment.Name + } else { + expectedSecretName = testData.expectedResultSecret.Name + } + + // Check if Secret has been created and has the correct data + secret := &corev1.Secret{} + err = cl.Get(context.TODO(), types.NamespacedName{Name: expectedSecretName, Namespace: namespace}, secret) + + if testData.expectedResultSecret == nil { + assert.Error(t, err) + assert.True(t, errors2.IsNotFound(err)) + } else { + assert.Equal(t, testData.expectedResultSecret.Data, secret.Data) + assert.Equal(t, testData.expectedResultSecret.Name, secret.Name) + assert.Equal(t, testData.expectedResultSecret.Type, secret.Type) + assert.Equal(t, testData.expectedResultSecret.Annotations[VersionAnnotation], secret.Annotations[VersionAnnotation]) + } + + //check if deployment has been restarted + deployment := &appsv1.Deployment{} + err = cl.Get(context.TODO(), types.NamespacedName{Name: testData.existingDeployment.Name, Namespace: namespace}, deployment) + + _, ok := deployment.Spec.Template.Annotations[RestartAnnotation] + if ok { + assert.True(t, testData.expectedRestart, "Expected deployment to restart but it did not") + } else { + assert.False(t, testData.expectedRestart, "Deployment was restarted but should not have been.") + } + }) + } +} + +func TestIsUpdatedSecret(t *testing.T) { + + secretName := "test-secret" + updatedSecrets := map[string]*corev1.Secret{ + "some_secret": &corev1.Secret{}, + } + assert.False(t, isUpdatedSecret(secretName, updatedSecrets)) + + updatedSecrets[secretName] = &corev1.Secret{} + assert.True(t, isUpdatedSecret(secretName, updatedSecrets)) +} + +func generateFields(username, password string) []*onepassword.ItemField { + fields := []*onepassword.ItemField{ + { + Label: "username", + Value: username, + }, + { + Label: "password", + Value: password, + }, + } + return fields +} diff --git a/pkg/onepassword/uuid.go b/pkg/onepassword/uuid.go new file mode 100644 index 0000000..4d250f0 --- /dev/null +++ b/pkg/onepassword/uuid.go @@ -0,0 +1,20 @@ +package onepassword + +// UUIDLength defines the required length of UUIDs +const UUIDLength = 26 + +// IsValidClientUUID returns true if the given client uuid is valid. +func IsValidClientUUID(uuid string) bool { + if len(uuid) != UUIDLength { + return false + } + + for _, c := range uuid { + valid := (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') + if !valid { + return false + } + } + + return true +} diff --git a/pkg/onepassword/volumes.go b/pkg/onepassword/volumes.go new file mode 100644 index 0000000..adf2e8b --- /dev/null +++ b/pkg/onepassword/volumes.go @@ -0,0 +1,29 @@ +package onepassword + +import corev1 "k8s.io/api/core/v1" + +func AreVolumesUsingSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret) bool { + for i := 0; i < len(volumes); i++ { + if secret := volumes[i].Secret; secret != nil { + secretName := secret.SecretName + _, ok := secrets[secretName] + if ok { + return true + } + } + } + return false +} + +func AppendUpdatedVolumeSecrets(volumes []corev1.Volume, secrets map[string]*corev1.Secret, updatedDeploymentSecrets map[string]*corev1.Secret) map[string]*corev1.Secret { + for i := 0; i < len(volumes); i++ { + if secret := volumes[i].Secret; secret != nil { + secretName := secret.SecretName + secret, ok := secrets[secretName] + if ok { + updatedDeploymentSecrets[secret.Name] = secret + } + } + } + return updatedDeploymentSecrets +} diff --git a/pkg/onepassword/volumes_test.go b/pkg/onepassword/volumes_test.go new file mode 100644 index 0000000..00d109c --- /dev/null +++ b/pkg/onepassword/volumes_test.go @@ -0,0 +1,43 @@ +package onepassword + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestAreVolmesUsingSecrets(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": &corev1.Secret{}, + "onepassword-api-key": &corev1.Secret{}, + } + + volumeSecretNames := []string{ + "onepassword-database-secret", + "onepassword-api-key", + "some_other_key", + } + + volumes := generateVolumes(volumeSecretNames) + + if !AreVolumesUsingSecrets(volumes, secretNamesToSearch) { + t.Errorf("Expected that volumes were using secrets but they were not detected.") + } +} + +func TestAreVolumesNotUsingSecrets(t *testing.T) { + secretNamesToSearch := map[string]*corev1.Secret{ + "onepassword-database-secret": &corev1.Secret{}, + "onepassword-api-key": &corev1.Secret{}, + } + + volumeSecretNames := []string{ + "some_other_key", + } + + volumes := generateVolumes(volumeSecretNames) + + if AreVolumesUsingSecrets(volumes, secretNamesToSearch) { + t.Errorf("Expected that volumes were not using secrets but they were detected.") + } +} diff --git a/pkg/utils/string.go b/pkg/utils/string.go new file mode 100644 index 0000000..ffe2871 --- /dev/null +++ b/pkg/utils/string.go @@ -0,0 +1,33 @@ +package utils + +import ( + "strconv" + "strings" +) + +func ContainsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +func RemoveString(slice []string, s string) (result []string) { + for _, item := range slice { + if item == s { + continue + } + result = append(result, item) + } + return +} + +func StringToBool(str string) (bool, error) { + restartDeploymentBool, err := strconv.ParseBool(strings.ToLower(str)) + if err != nil { + return false, err + } + return restartDeploymentBool, nil +}