Option to automatically deploy 1Password Connect via the operator

This commit is contained in:
jillianwilson
2021-01-12 16:42:22 -04:00
parent 9f708729a8
commit eebb90e43b
9 changed files with 326 additions and 6 deletions

View File

@@ -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"]
ENTRYPOINT ["/manager"]

View File

@@ -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:

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -17,7 +17,6 @@ spec:
- name: onepassword-connect-operator
image: 1password/onepassword-operator
command: ["/manager"]
imagePullPolicy: Never
env:
- name: WATCH_NAMESPACE
value: "default"

2
go.sum
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}
}