From eebb90e43be4ff3c9493096a935d4698e49acfbb Mon Sep 17 00:00:00 2001 From: jillianwilson Date: Tue, 12 Jan 2021 16:42:22 -0400 Subject: [PATCH] Option to automatically deploy 1Password Connect via the operator --- Dockerfile | 3 +- README.md | 33 +++++++- cmd/manager/main.go | 35 +++++++++ deploy/connect/deployment.yaml | 69 ++++++++++++++++ deploy/connect/service.yaml | 16 ++++ deploy/operator.yaml | 1 - go.sum | 2 + pkg/onepassword/connect_setup.go | 108 ++++++++++++++++++++++++++ pkg/onepassword/connect_setup_test.go | 65 ++++++++++++++++ 9 files changed, 326 insertions(+), 6 deletions(-) create mode 100644 deploy/connect/deployment.yaml create mode 100644 deploy/connect/service.yaml create mode 100644 pkg/onepassword/connect_setup.go create mode 100644 pkg/onepassword/connect_setup_test.go diff --git a/Dockerfile b/Dockerfile index 086e861..070601a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,5 +20,6 @@ FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER nonroot:nonroot +COPY deploy/connect/ deploy/connect/ -ENTRYPOINT ["/manager"] \ No newline at end of file +ENTRYPOINT ["/manager"] diff --git a/README.md b/README.md index 7cb1808..081ba1b 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,33 @@ Prerequisites: - [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/) - [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - [docker installed](https://docs.docker.com/get-docker/) -- [1Password Connect has been setup with an API token issued to be used with the operator.](https://support.1password.com/cs/connect) -- [1Password Connect deployed to Kubernetes](https://support.1password.com/cs/connect) +- [Generated a 1password-credentials.json file and issued a 1Password Connect API Token for the K8s Operator integration](https://support.b5dev.com/cs/connect) +- [1Password Connect deployed to Kubernetes](https://support.b5dev.com/cs/connect-deploy-kubernetes/#step-2-deploy-a-connect-server). **NOTE**: If customization of the 1Password Connect deployment is not required you can skip this prerequisite. +### Quickstart for Deploying 1Password Connect to Kubernetes + +If 1Password Connect is already running, you can skip this step. This guide will provide a quickstart option for deploying a default configuration of 1Password Connect via starting the deploying the 1Password Connect Operator, however it is recommended that you instead deploy your own manifest file if customization of the 1Password Connect deployment is desired. + +Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session: + +```bash +$ cat 1password-credentials.json | base64 | \ + tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session +``` + +Create a Kubernetes secret from the op-session file: +```bash + +$ kubectl create secret generic op-credentials --from-file=op-session \ + --dry-run=client -o yaml | kubectl apply -f - +``` + +Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`: +```yaml +- name: MANAGE_CONNECT + value: "true" +``` +Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the `default` namespace. ### Kubernetes Operator Deployment **Create Kubernetes Secret for OP_CONNECT_TOKEN** @@ -55,11 +79,12 @@ and update the image pull policy to `Always` imagePullPolicy: Always ``` -To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the deployment yaml: +To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml: - **WATCH_NAMESPACE:** comma separated list of what Namespaces to watch for changes. - **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect. -- **POLLING_INTERVAL** (default: 600)**:** The number of seconds ****the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. +- **POLLING_INTERVAL** (default: 600)**:** The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect. +- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace. Apply the deployment file: diff --git a/cmd/manager/main.go b/cmd/manager/main.go index fef1436..200b76d 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -41,6 +41,7 @@ import ( ) const envPollingIntervalVariable = "POLLING_INTERVAL" +const manageConnect = "MANAGE_CONNECT" const defaultPollingInterval = 600 // Change below variables to serve metrics on different host or port. @@ -131,6 +132,27 @@ func main() { os.Exit(1) } + //Setup 1PasswordConnect + if shouldManageConnect() { + log.Info("Automated Connect Management Enabled") + go func() { + connectStarted := false + for connectStarted == false { + err := op.SetupConnect(mgr.GetClient()) + // Cache Not Started is an acceptable error. Retry until cache is started. + if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { + log.Error(err, "") + os.Exit(1) + } + if err == nil { + connectStarted = true + } + } + }() + } else { + log.Info("Automated Connect Management Disabled") + } + // Setup One Password Client opConnectClient, err := connect.NewClientFromEnvironment() @@ -249,3 +271,16 @@ func getPollingIntervalForUpdatingSecrets() time.Duration { log.Info(fmt.Sprintf("Using default polling interval of %v seconds", defaultPollingInterval)) return time.Duration(defaultPollingInterval) * time.Second } + +func shouldManageConnect() bool { + shouldManageConnect, found := os.LookupEnv(manageConnect) + if found { + shouldManageConnectBool, err := strconv.ParseBool(strings.ToLower(shouldManageConnect)) + if err != nil { + log.Error(err, "") + os.Exit(1) + } + return shouldManageConnectBool + } + return false +} diff --git a/deploy/connect/deployment.yaml b/deploy/connect/deployment.yaml new file mode 100644 index 0000000..cc4830f --- /dev/null +++ b/deploy/connect/deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: onepassword-connect + namespace: default +spec: + selector: + matchLabels: + app: onepassword-connect + template: + metadata: + labels: + app: onepassword-connect + version: "0.3.0" + spec: + volumes: + - name: shared-data + emptyDir: {} + - name: credentials + secret: + secretName: op-credentials + initContainers: + - name: sqlite-permissions + image: alpine:3.12 + command: + - "/bin/sh" + - "-c" + args: + - "mkdir -p /home/opuser/.op/data && chown -R 999 /home/opuser && chmod -R 700 /home/opuser && chmod -f -R 600 /home/opuser/.op/config || :" + volumeMounts: + - mountPath: /home/opuser/.op/data + name: shared-data + containers: + - name: connect-api + image: 1password/connect-api:latest + resources: + limits: + memory: "128Mi" + cpu: "0.2" + ports: + - containerPort: 8080 + env: + - name: OP_SESSION + valueFrom: + secretKeyRef: + name: op-credentials + key: op-session + volumeMounts: + - mountPath: /home/opuser/.op/data + name: shared-data + - name: connect-sync + image: 1password/connect-sync:latest + resources: + limits: + memory: "128Mi" + cpu: "0.2" + ports: + - containerPort: 8081 + env: + - name: OP_HTTP_PORT + value: "8081" + - name: OP_SESSION + valueFrom: + secretKeyRef: + name: op-credentials + key: op-session + volumeMounts: + - mountPath: /home/opuser/.op/data + name: shared-data diff --git a/deploy/connect/service.yaml b/deploy/connect/service.yaml new file mode 100644 index 0000000..441d77b --- /dev/null +++ b/deploy/connect/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: onepassword-connect + namespace: default +spec: + type: NodePort + selector: + app: onepassword-connect + ports: + - port: 8080 + name: connect-api + nodePort: 31080 + - port: 8081 + name: connect-sync + nodePort: 31081 diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 4da8555..deb3d73 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -17,7 +17,6 @@ spec: - name: onepassword-connect-operator image: 1password/onepassword-operator command: ["/manager"] - imagePullPolicy: Never env: - name: WATCH_NAMESPACE value: "default" diff --git a/go.sum b/go.sum index fbc1b30..58fa4cd 100644 --- a/go.sum +++ b/go.sum @@ -1446,6 +1446,7 @@ k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA= k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/apimachinery v0.19.2 h1:5Gy9vQpAGTKHPVOh5c4plE274X8D/6cuEiTO2zve7tc= k8s.io/apimachinery v0.19.3 h1:bpIQXlKjB4cB/oNpnNnV+BybGPR7iP5oYpsOTEJ4hgc= +k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ= k8s.io/apiserver v0.0.0-20190918160949-bfa5e2e684ad/go.mod h1:XPCXEwhjaFN29a8NldXA901ElnKeKLrLtREO9ZhFyhg= k8s.io/apiserver v0.0.0-20191122221311-9d521947b1e1/go.mod h1:RbsZY5zzBIWnz4KbctZsTVjwIuOpTp4Z8oCgFHN4kZQ= k8s.io/apiserver v0.16.7/go.mod h1:/5zSatF30/L9zYfMTl55jzzOnx7r/gGv5a5wtRp8yAw= @@ -1539,6 +1540,7 @@ sigs.k8s.io/controller-runtime v0.5.2 h1:pyXbUfoTo+HA3jeIfr0vgi+1WtmNh0CwlcnQGLX sigs.k8s.io/controller-runtime v0.5.2/go.mod h1:JZUwSMVbxDupo0lTJSSFP5pimEyxGynROImSsqIOx1A= sigs.k8s.io/controller-runtime v0.6.0 h1:Fzna3DY7c4BIP6KwfSlrfnj20DJ+SeMBK8HSFvOk9NM= sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo= +sigs.k8s.io/controller-runtime v0.7.0 h1:bU20IBBEPccWz5+zXpLnpVsgBYxqclaHu1pVDl/gEt8= sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= sigs.k8s.io/controller-tools v0.2.8/go.mod h1:9VKHPszmf2DHz/QmHkcfZoewO6BL7pPs9uAiBVsaJSE= sigs.k8s.io/controller-tools v0.3.0/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI= diff --git a/pkg/onepassword/connect_setup.go b/pkg/onepassword/connect_setup.go new file mode 100644 index 0000000..37c5751 --- /dev/null +++ b/pkg/onepassword/connect_setup.go @@ -0,0 +1,108 @@ +package onepassword + +import ( + "context" + "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) error { + err := setupService(kubeClient, servicePath) + if err != nil { + return err + } + + err = setupDeployment(kubeClient, deploymentPath) + if err != nil { + return err + } + + return nil +} + +func setupDeployment(kubeClient client.Client, deploymentPath string) error { + existingDeployment := &appsv1.Deployment{} + + // check if deployment has already been created + err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingDeployment) + if err != nil { + if errors.IsNotFound(err) { + logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") + return createDeployment(kubeClient, deploymentPath) + } + } + return err +} + +func createDeployment(kubeClient client.Client, deploymentPath string) error { + deployment, err := getDeploymentToCreate(deploymentPath) + if err != nil { + return err + } + + err = kubeClient.Create(context.Background(), deployment) + if err != nil { + return err + } + + return nil +} + +func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) { + f, err := os.Open(deploymentPath) + if err != nil { + return nil, err + } + deployment := &appsv1.Deployment{} + + err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) + if err != nil { + return nil, err + } + return deployment, nil +} + +func setupService(kubeClient client.Client, servicePath string) error { + existingService := &corev1.Service{} + + //check if service has already been created + err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingService) + if err != nil { + if errors.IsNotFound(err) { + logConnectSetup.Info("No existing Connect service found. Creating Service") + return createService(kubeClient, servicePath) + } + } + return err +} + +func createService(kubeClient client.Client, servicePath string) error { + f, err := os.Open(servicePath) + if err != nil { + return err + } + service := &corev1.Service{} + + 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..d917e46 --- /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") + + 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") + + 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) + } +}