mirror of
				https://github.com/1Password/onepassword-operator.git
				synced 2025-10-31 03:39:39 +00:00 
			
		
		
		
	Compare commits
	
		
			55 Commits
		
	
	
		
			deploy-1.2
			...
			v1.2.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | b68d9a5d79 | ||
|   | befcaae457 | ||
|   | b24aa48bd6 | ||
|   | b1e251dee6 | ||
|   | a34c6e8b38 | ||
|   | b16960057a | ||
|   | 285496dc7e | ||
|   | f38cf7e1c2 | ||
|   | bb7a0c8ca9 | ||
|   | 302653832e | ||
|   | a1bcfdfdcb | ||
|   | c0f1632638 | ||
|   | c46065fa7a | ||
|   | 5d229c42d5 | ||
|   | c7235b4f09 | ||
|   | 5183fc129a | ||
|   | 7d619165b2 | ||
|   | 0363ae1e4e | ||
|   | d9e003bdb7 | ||
|   | b25f943b3a | ||
|   | 5fab662424 | ||
|   | d807e92c36 | ||
|   | 244771717c | ||
|   | 7aeb36e383 | ||
|   | 5c2f840623 | ||
|   | 670040477e | ||
|   | a45a310611 | ||
|   | d80e8dd799 | ||
|   | 88728909ff | ||
|   | e365ebfdfa | ||
|   | 2c4b4df01a | ||
|   | 49d984c6f2 | ||
|   | 72cad7284c | ||
|   | 0193a98681 | ||
|   | f241d7423d | ||
|   | 6043e0da0b | ||
|   | 753cc5e9a3 | ||
|   | 8cfe98073e | ||
|   | c0037526b0 | ||
|   | 96b42e7c52 | ||
|   | 579b5848da | ||
|   | dff934cbc3 | ||
|   | 2096f4440f | ||
|   | b3fc707337 | ||
|   | 313cd1169b | ||
|   | b50d864b50 | ||
|   | 1643385d9b | ||
|   | 9441214733 | ||
|   | 7e4e988813 | ||
|   | fb1262f1bd | ||
|   | 68f084080e | ||
|   | a428fe7462 | ||
|   | ea2d1f8a09 | ||
|   | bd96d50a9b | ||
|   | 859c9e3462 | 
							
								
								
									
										25
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -12,6 +12,31 @@ | |||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | [//]: # (START/v1.2.0) | ||||||
|  | # v1.2.0 | ||||||
|  |  | ||||||
|  | ## Features | ||||||
|  |   * Support secrets provisioned through FromEnv. {#74} | ||||||
|  |   * Support configuration of Kubernetes Secret type. {#87} | ||||||
|  |   * Improved logging. (#72) | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # (START/v1.1.0) | ||||||
|  | # v1.1.0 | ||||||
|  |  | ||||||
|  | ## Fixes | ||||||
|  |  * Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66} | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | [//]: # (START/v1.0.2) | ||||||
|  | # v1.0.2 | ||||||
|  |  | ||||||
|  | ## Fixes | ||||||
|  |  * Name normalizer added to handle non-conforming item names. | ||||||
|  |  | ||||||
|  | --- | ||||||
|  |  | ||||||
| [//]: # (START/v1.0.1) | [//]: # (START/v1.0.1) | ||||||
| # v1.0.1 | # v1.0.1 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							| @@ -20,12 +20,12 @@ test/coverage:	## Run test suite with coverage report | |||||||
| 	go test -v ./... -cover | 	go test -v ./... -cover | ||||||
|  |  | ||||||
| build:	## Build operator Docker image | build:	## Build operator Docker image | ||||||
| 	@docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) | 	@docker build -f Dockerfile --build-arg operator_version=$(curVersion) -t $(DOCKER_IMG_TAG) . | ||||||
| 	@echo "Successfully built and tagged image." | 	@echo "Successfully built and tagged image." | ||||||
| 	@echo "Tag: $(DOCKER_IMG_TAG)" | 	@echo "Tag: $(DOCKER_IMG_TAG)" | ||||||
|  |  | ||||||
| build/local:	## Build local version of the operator Docker image | build/local:	## Build local version of the operator Docker image | ||||||
| 	@docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG) | 	@docker build -f Dockerfile -t local/$(DOCKER_IMG_TAG) . | ||||||
|  |  | ||||||
| build/binary: clean	## Build operator binary | build/binary: clean	## Build operator binary | ||||||
| 	@mkdir -p dist | 	@mkdir -p dist | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								README.md
									
									
									
									
									
								
							| @@ -30,14 +30,13 @@ If 1Password Connect is already running, you can skip this step. This guide will | |||||||
| Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session: | Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ cat 1password-credentials.json | base64 | \ | cat 1password-credentials.json | base64 | \ | ||||||
|   tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session |   tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Create a Kubernetes secret from the op-session file: | Create a Kubernetes secret from the op-session file: | ||||||
| ```bash | ```bash | ||||||
|  | kubectl create secret generic op-credentials --from-file=1password-credentials.json=op-session | ||||||
| $  kubectl create secret generic op-credentials --from-file=1password-credentials.json |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`: | Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`: | ||||||
| @@ -53,12 +52,12 @@ Adding this environment variable will have the operator automatically deploy a d | |||||||
| "Create a Connect token for the operator and save it as a Kubernetes Secret:  | "Create a Connect token for the operator and save it as a Kubernetes Secret:  | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>" | kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>" | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command: | If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command: | ||||||
| ```bash | ```bash | ||||||
| $ kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>) | kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>) | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| [More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens) | [More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens) | ||||||
| @@ -68,13 +67,13 @@ $ kubectl create secret generic onepassword-token --from-literal=token=$(op crea | |||||||
| We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder. | We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder. | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl apply -f deploy/permissions.yaml | kubectl apply -f deploy/permissions.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| **Create Custom One Password Secret Resource** | **Create Custom One Password Secret Resource** | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml | kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| **Deploying the Operator** | **Deploying the Operator** | ||||||
| @@ -84,9 +83,9 @@ An sample Deployment yaml can be found at `/deploy/operator.yaml`. | |||||||
|  |  | ||||||
| To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator 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. | - **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. | - **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes. | ||||||
|  | - **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. | - **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. | ||||||
| - **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. | - **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section. | ||||||
|  |  | ||||||
| @@ -112,13 +111,13 @@ spec: | |||||||
| Deploy the OnePasswordItem to Kubernetes: | Deploy the OnePasswordItem to Kubernetes: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl apply -f <your_item>.yaml | kubectl apply -f <your_item>.yaml | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| To test that the Kubernetes Secret check that the following command returns a secret: | To test that the Kubernetes Secret check that the following command returns a secret: | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| $ kubectl get secret <secret_name> | kubectl get secret <secret_name> | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret. | Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret. | ||||||
| @@ -144,7 +143,12 @@ If a 1Password Item that is linked to a Kubernetes Secret is updated within the | |||||||
| --- | --- | ||||||
| **NOTE** | **NOTE** | ||||||
|  |  | ||||||
| If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item. Furthermore, titles that include white space characters cannot be used. | If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.  | ||||||
|  |  | ||||||
|  | Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format: | ||||||
|  |  - Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed | ||||||
|  |  - All whitespaces between words will be replaced by `-` | ||||||
|  |  - All the letters will be lower-cased. | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -83,9 +83,11 @@ func main() { | |||||||
|  |  | ||||||
| 	printVersion() | 	printVersion() | ||||||
|  |  | ||||||
| 	namespace, err := k8sutil.GetWatchNamespace() | 	namespace := os.Getenv(k8sutil.WatchNamespaceEnvVar) | ||||||
|  |  | ||||||
|  | 	deploymentNamespace, err := k8sutil.GetOperatorNamespace() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error(err, "Failed to get watch namespace") | 		log.Error(err, "Failed to get namespace") | ||||||
| 		os.Exit(1) | 		os.Exit(1) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -139,7 +141,7 @@ func main() { | |||||||
| 		go func() { | 		go func() { | ||||||
| 			connectStarted := false | 			connectStarted := false | ||||||
| 			for connectStarted == false { | 			for connectStarted == false { | ||||||
| 				err := op.SetupConnect(mgr.GetClient()) | 				err := op.SetupConnect(mgr.GetClient(), deploymentNamespace) | ||||||
| 				// Cache Not Started is an acceptable error. Retry until cache is started. | 				// Cache Not Started is an acceptable error. Retry until cache is started. | ||||||
| 				if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { | 				if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { | ||||||
| 					log.Error(err, "") | 					log.Error(err, "") | ||||||
| @@ -176,7 +178,10 @@ func main() { | |||||||
| 				ticker.Stop() | 				ticker.Stop() | ||||||
| 				return | 				return | ||||||
| 			case <-ticker.C: | 			case <-ticker.C: | ||||||
| 				updatedSecretsPoller.UpdateKubernetesSecretsTask() | 				err := updatedSecretsPoller.UpdateKubernetesSecretsTask() | ||||||
|  | 				if err != nil { | ||||||
|  | 					log.Error(err, "error running update kubernetes secret task") | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ apiVersion: apps/v1 | |||||||
| kind: Deployment | kind: Deployment | ||||||
| metadata: | metadata: | ||||||
|   name: onepassword-connect |   name: onepassword-connect | ||||||
|   namespace: default |  | ||||||
| spec: | spec: | ||||||
|   selector: |   selector: | ||||||
|     matchLabels: |     matchLabels: | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ apiVersion: v1 | |||||||
| kind: Service | kind: Service | ||||||
| metadata: | metadata: | ||||||
|   name: onepassword-connect |   name: onepassword-connect | ||||||
|   namespace: default |  | ||||||
| spec: | spec: | ||||||
|   type: NodePort |   type: NodePort | ||||||
|   selector: |   selector: | ||||||
|   | |||||||
| @@ -39,4 +39,7 @@ spec: | |||||||
|           status: |           status: | ||||||
|             description: OnePasswordItemStatus defines the observed state of OnePasswordItem |             description: OnePasswordItemStatus defines the observed state of OnePasswordItem | ||||||
|             type: object |             type: object | ||||||
|  |           type: | ||||||
|  |             description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types' | ||||||
|  |             type: string | ||||||
|         type: object |         type: object | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ type OnePasswordItemStatus struct { | |||||||
| type OnePasswordItem struct { | type OnePasswordItem struct { | ||||||
| 	metav1.TypeMeta   `json:",inline"` | 	metav1.TypeMeta   `json:",inline"` | ||||||
| 	metav1.ObjectMeta `json:"metadata,omitempty"` | 	metav1.ObjectMeta `json:"metadata,omitempty"` | ||||||
|  | 	Type              string `json:"type,omitempty"` | ||||||
|  |  | ||||||
| 	Spec   OnePasswordItemSpec   `json:"spec,omitempty"` | 	Spec   OnePasswordItemSpec   `json:"spec,omitempty"` | ||||||
| 	Status OnePasswordItemStatus `json:"status,omitempty"` | 	Status OnePasswordItemStatus `json:"status,omitempty"` | ||||||
|   | |||||||
| @@ -191,6 +191,9 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat | |||||||
| 	reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) | 	reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) | ||||||
|  |  | ||||||
| 	secretName := annotations[op.NameAnnotation] | 	secretName := annotations[op.NameAnnotation] | ||||||
|  | 	secretLabels := map[string]string(nil) | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
| 	if len(secretName) == 0 { | 	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.") | 		reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.") | ||||||
| 		return nil | 		return nil | ||||||
| @@ -201,5 +204,5 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat | |||||||
| 		return fmt.Errorf("Failed to retrieve item: %v", err) | 		return fmt.Errorf("Failed to retrieve item: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation]) | 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, annotations) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -258,7 +258,7 @@ var tests = []testReconcileItem{ | |||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		testName: "Test Do not update if OnePassword Item Version has not changed", | 		testName: "Test Do not update if Annotations have not changed", | ||||||
| 		deploymentResource: &appsv1.Deployment{ | 		deploymentResource: &appsv1.Deployment{ | ||||||
| 			TypeMeta: metav1.TypeMeta{ | 			TypeMeta: metav1.TypeMeta{ | ||||||
| 				Kind:       deploymentKind, | 				Kind:       deploymentKind, | ||||||
| @@ -271,6 +271,7 @@ var tests = []testReconcileItem{ | |||||||
| 					op.ItemPathAnnotation: itemPath, | 					op.ItemPathAnnotation: itemPath, | ||||||
| 					op.NameAnnotation:     name, | 					op.NameAnnotation:     name, | ||||||
| 				}, | 				}, | ||||||
|  | 				Labels: map[string]string{}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		existingSecret: &corev1.Secret{ | 		existingSecret: &corev1.Secret{ | ||||||
| @@ -279,6 +280,8 @@ var tests = []testReconcileItem{ | |||||||
| 				Namespace: namespace, | 				Namespace: namespace, | ||||||
| 				Annotations: map[string]string{ | 				Annotations: map[string]string{ | ||||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
|  | 					op.NameAnnotation:     name, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| @@ -290,7 +293,10 @@ var tests = []testReconcileItem{ | |||||||
| 				Namespace: namespace, | 				Namespace: namespace, | ||||||
| 				Annotations: map[string]string{ | 				Annotations: map[string]string{ | ||||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
|  | 					op.NameAnnotation:     name, | ||||||
| 				}, | 				}, | ||||||
|  | 				Labels: map[string]string(nil), | ||||||
| 			}, | 			}, | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| 		}, | 		}, | ||||||
| @@ -323,6 +329,7 @@ var tests = []testReconcileItem{ | |||||||
| 					op.VersionAnnotation: "456", | 					op.VersionAnnotation: "456", | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
|  | 			Type: corev1.SecretType(""), | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| 		}, | 		}, | ||||||
| 		expectedError: nil, | 		expectedError: nil, | ||||||
| @@ -334,6 +341,7 @@ var tests = []testReconcileItem{ | |||||||
| 					op.VersionAnnotation: fmt.Sprint(version), | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
|  | 			Type: corev1.SecretType(""), | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| 		}, | 		}, | ||||||
| 		opItem: map[string]string{ | 		opItem: map[string]string{ | ||||||
| @@ -367,6 +375,7 @@ var tests = []testReconcileItem{ | |||||||
| 					op.VersionAnnotation: fmt.Sprint(version), | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
|  | 			Type: corev1.SecretType(""), | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| 		}, | 		}, | ||||||
| 		opItem: map[string]string{ | 		opItem: map[string]string{ | ||||||
|   | |||||||
| @@ -144,12 +144,15 @@ func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem | |||||||
|  |  | ||||||
| func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { | func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { | ||||||
| 	secretName := resource.GetName() | 	secretName := resource.GetName() | ||||||
| 	autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation] | 	labels := resource.Labels | ||||||
|  | 	annotations := resource.Annotations | ||||||
|  | 	secretType := resource.Type | ||||||
|  | 	autoRestart := annotations[op.RestartDeploymentsAnnotation] | ||||||
|  |  | ||||||
| 	item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath) | 	item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("Failed to retrieve item: %v", err) | 		return fmt.Errorf("Failed to retrieve item: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart) | 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, secretType, annotations) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/1Password/onepassword-operator/pkg/kubernetessecrets" | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/mocks" | 	"github.com/1Password/onepassword-operator/pkg/mocks" | ||||||
| 	op "github.com/1Password/onepassword-operator/pkg/onepassword" | 	op "github.com/1Password/onepassword-operator/pkg/onepassword" | ||||||
|  |  | ||||||
| @@ -31,6 +32,9 @@ const ( | |||||||
| 	itemId                    = "nwrhuano7bcwddcviubpp4mhfq" | 	itemId                    = "nwrhuano7bcwddcviubpp4mhfq" | ||||||
| 	username                  = "test-user" | 	username                  = "test-user" | ||||||
| 	password                  = "QmHumKc$mUeEem7caHtbaBaJ" | 	password                  = "QmHumKc$mUeEem7caHtbaBaJ" | ||||||
|  | 	firstHost                 = "http://localhost:8080" | ||||||
|  | 	awsKey                    = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" | ||||||
|  | 	iceCream                  = "freezing blue 20%" | ||||||
| 	userKey                   = "username" | 	userKey                   = "username" | ||||||
| 	passKey                   = "password" | 	passKey                   = "password" | ||||||
| 	version                   = 123 | 	version                   = 123 | ||||||
| @@ -117,6 +121,7 @@ var tests = []testReconcileItem{ | |||||||
| 				Namespace: namespace, | 				Namespace: namespace, | ||||||
| 				Annotations: map[string]string{ | 				Annotations: map[string]string{ | ||||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| @@ -128,6 +133,7 @@ var tests = []testReconcileItem{ | |||||||
| 				Namespace: namespace, | 				Namespace: namespace, | ||||||
| 				Annotations: map[string]string{ | 				Annotations: map[string]string{ | ||||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| @@ -147,6 +153,11 @@ var tests = []testReconcileItem{ | |||||||
| 			ObjectMeta: metav1.ObjectMeta{ | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
| 				Name:      name, | 				Name:      name, | ||||||
| 				Namespace: namespace, | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 				Labels: map[string]string{}, | ||||||
| 			}, | 			}, | ||||||
| 			Spec: onepasswordv1.OnePasswordItemSpec{ | 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||||
| 				ItemPath: itemPath, | 				ItemPath: itemPath, | ||||||
| @@ -158,7 +169,9 @@ var tests = []testReconcileItem{ | |||||||
| 				Namespace: namespace, | 				Namespace: namespace, | ||||||
| 				Annotations: map[string]string{ | 				Annotations: map[string]string{ | ||||||
| 					op.VersionAnnotation:  "456", | 					op.VersionAnnotation:  "456", | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
| 				}, | 				}, | ||||||
|  | 				Labels: map[string]string{}, | ||||||
| 			}, | 			}, | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| 		}, | 		}, | ||||||
| @@ -169,8 +182,63 @@ var tests = []testReconcileItem{ | |||||||
| 				Namespace: namespace, | 				Namespace: namespace, | ||||||
| 				Annotations: map[string]string{ | 				Annotations: map[string]string{ | ||||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 				Labels: map[string]string{}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: "Test Updating Type of Existing Kubernetes Secret using OnePasswordItem", | ||||||
|  | 		customResource: &onepasswordv1.OnePasswordItem{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       onePasswordItemKind, | ||||||
|  | 				APIVersion: onePasswordItemAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 				Labels: map[string]string{}, | ||||||
|  | 			}, | ||||||
|  | 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||||
|  | 				ItemPath: itemPath, | ||||||
|  | 			}, | ||||||
|  | 			Type: string(corev1.SecretTypeBasicAuth), | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 				Labels: map[string]string{}, | ||||||
|  | 			}, | ||||||
|  | 			Type: corev1.SecretTypeBasicAuth, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation:  fmt.Sprint(version), | ||||||
|  | 					op.ItemPathAnnotation: itemPath, | ||||||
|  | 				}, | ||||||
|  | 				Labels: map[string]string{}, | ||||||
|  | 			}, | ||||||
|  | 			Type: corev1.SecretTypeBasicAuth, | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| 		}, | 		}, | ||||||
| 		opItem: map[string]string{ | 		opItem: map[string]string{ | ||||||
| @@ -192,6 +260,7 @@ var tests = []testReconcileItem{ | |||||||
| 			Spec: onepasswordv1.OnePasswordItemSpec{ | 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||||
| 				ItemPath: itemPath, | 				ItemPath: itemPath, | ||||||
| 			}, | 			}, | ||||||
|  | 			Type: "custom", | ||||||
| 		}, | 		}, | ||||||
| 		existingSecret: nil, | 		existingSecret: nil, | ||||||
| 		expectedError:  nil, | 		expectedError:  nil, | ||||||
| @@ -203,6 +272,7 @@ var tests = []testReconcileItem{ | |||||||
| 					op.VersionAnnotation: fmt.Sprint(version), | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
|  | 			Type: corev1.SecretType("custom"), | ||||||
| 			Data: expectedSecretData, | 			Data: expectedSecretData, | ||||||
| 		}, | 		}, | ||||||
| 		opItem: map[string]string{ | 		opItem: map[string]string{ | ||||||
| @@ -210,6 +280,164 @@ var tests = []testReconcileItem{ | |||||||
| 			passKey: password, | 			passKey: password, | ||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: "Error if secret type is changed", | ||||||
|  | 		customResource: &onepasswordv1.OnePasswordItem{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       onePasswordItemKind, | ||||||
|  | 				APIVersion: onePasswordItemAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 			}, | ||||||
|  | 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||||
|  | 				ItemPath: itemPath, | ||||||
|  | 			}, | ||||||
|  | 			Type: "custom", | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Type: corev1.SecretTypeOpaque, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		expectedError: kubernetessecrets.ErrCannotUpdateSecretType, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      name, | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Type: corev1.SecretTypeOpaque, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: "Secret from 1Password item with invalid K8s labels", | ||||||
|  | 		customResource: &onepasswordv1.OnePasswordItem{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       onePasswordItemKind, | ||||||
|  | 				APIVersion: onePasswordItemAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      "!my sECReT it3m%", | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 			}, | ||||||
|  | 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||||
|  | 				ItemPath: itemPath, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: nil, | ||||||
|  | 		expectedError:  nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      "my-secret-it3m", | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: expectedSecretData, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey: username, | ||||||
|  | 			passKey: password, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: "Secret from 1Password item with fields and sections that have invalid K8s labels", | ||||||
|  | 		customResource: &onepasswordv1.OnePasswordItem{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       onePasswordItemKind, | ||||||
|  | 				APIVersion: onePasswordItemAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      "!my sECReT it3m%", | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 			}, | ||||||
|  | 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||||
|  | 				ItemPath: itemPath, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: nil, | ||||||
|  | 		expectedError:  nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      "my-secret-it3m", | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: map[string][]byte{ | ||||||
|  | 				"password":       []byte(password), | ||||||
|  | 				"username":       []byte(username), | ||||||
|  | 				"first-host":     []byte(firstHost), | ||||||
|  | 				"AWS-Access-Key": []byte(awsKey), | ||||||
|  | 				"ice-cream-type": []byte(iceCream), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey:            username, | ||||||
|  | 			passKey:            password, | ||||||
|  | 			"first host":       firstHost, | ||||||
|  | 			"AWS Access Key":   awsKey, | ||||||
|  | 			"😄 ice-cream type": iceCream, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 	{ | ||||||
|  | 		testName: "Secret from 1Password item with `-`, `_` and `.`", | ||||||
|  | 		customResource: &onepasswordv1.OnePasswordItem{ | ||||||
|  | 			TypeMeta: metav1.TypeMeta{ | ||||||
|  | 				Kind:       onePasswordItemKind, | ||||||
|  | 				APIVersion: onePasswordItemAPIVersion, | ||||||
|  | 			}, | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      "!.my_sECReT.it3m%-_", | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 			}, | ||||||
|  | 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||||
|  | 				ItemPath: itemPath, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		existingSecret: nil, | ||||||
|  | 		expectedError:  nil, | ||||||
|  | 		expectedResultSecret: &corev1.Secret{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name:      "my-secret.it3m", | ||||||
|  | 				Namespace: namespace, | ||||||
|  | 				Annotations: map[string]string{ | ||||||
|  | 					op.VersionAnnotation: fmt.Sprint(version), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			Data: map[string][]byte{ | ||||||
|  | 				"password":          []byte(password), | ||||||
|  | 				"username":          []byte(username), | ||||||
|  | 				"first-host":        []byte(firstHost), | ||||||
|  | 				"AWS-Access-Key":    []byte(awsKey), | ||||||
|  | 				"-_ice_cream.type.": []byte(iceCream), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		opItem: map[string]string{ | ||||||
|  | 			userKey:               username, | ||||||
|  | 			passKey:               password, | ||||||
|  | 			"first host":          firstHost, | ||||||
|  | 			"AWS Access Key":      awsKey, | ||||||
|  | 			"😄 -_ice_cream.type.": iceCream, | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestReconcileOnePasswordItem(t *testing.T) { | func TestReconcileOnePasswordItem(t *testing.T) { | ||||||
| @@ -241,7 +469,10 @@ func TestReconcileOnePasswordItem(t *testing.T) { | |||||||
| 			mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { | 			mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { | ||||||
|  |  | ||||||
| 				item := onepassword.Item{} | 				item := onepassword.Item{} | ||||||
| 				item.Fields = generateFields(testData.opItem["username"], testData.opItem["password"]) | 				item.Fields = []*onepassword.ItemField{} | ||||||
|  | 				for k, v := range testData.opItem { | ||||||
|  | 					item.Fields = append(item.Fields, &onepassword.ItemField{Label: k, Value: v}) | ||||||
|  | 				} | ||||||
| 				item.Version = version | 				item.Version = version | ||||||
| 				item.Vault.ID = vaultUUID | 				item.Vault.ID = vaultUUID | ||||||
| 				item.ID = uuid | 				item.ID = uuid | ||||||
| @@ -257,8 +488,8 @@ func TestReconcileOnePasswordItem(t *testing.T) { | |||||||
| 			// watched resource . | 			// watched resource . | ||||||
| 			req := reconcile.Request{ | 			req := reconcile.Request{ | ||||||
| 				NamespacedName: types.NamespacedName{ | 				NamespacedName: types.NamespacedName{ | ||||||
| 					Name:      name, | 					Name:      testData.customResource.ObjectMeta.Name, | ||||||
| 					Namespace: namespace, | 					Namespace: testData.customResource.ObjectMeta.Namespace, | ||||||
| 				}, | 				}, | ||||||
| 			} | 			} | ||||||
| 			_, err := r.Reconcile(req) | 			_, err := r.Reconcile(req) | ||||||
|   | |||||||
| @@ -4,12 +4,21 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"reflect" | ||||||
|  |  | ||||||
|  | 	errs "errors" | ||||||
|  |  | ||||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | 	"github.com/1Password/connect-sdk-go/onepassword" | ||||||
| 	"github.com/1Password/onepassword-operator/pkg/utils" | 	"github.com/1Password/onepassword-operator/pkg/utils" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/api/errors" | 	"k8s.io/apimachinery/pkg/api/errors" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	kubeValidate "k8s.io/apimachinery/pkg/util/validation" | ||||||
|  |  | ||||||
| 	kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" | 	kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" | ||||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" | 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
| ) | ) | ||||||
| @@ -21,24 +30,33 @@ const restartAnnotation = OnepasswordPrefix + "/last-restarted" | |||||||
| const ItemPathAnnotation = OnepasswordPrefix + "/item-path" | const ItemPathAnnotation = OnepasswordPrefix + "/item-path" | ||||||
| const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" | const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" | ||||||
|  |  | ||||||
|  | var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable") | ||||||
|  |  | ||||||
| var log = logf.Log | var log = logf.Log | ||||||
|  |  | ||||||
| func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string) error { | func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, secretAnnotations map[string]string) error { | ||||||
|  |  | ||||||
| 	itemVersion := fmt.Sprint(item.Version) | 	itemVersion := fmt.Sprint(item.Version) | ||||||
| 	annotations := map[string]string{ |  | ||||||
| 		VersionAnnotation:  itemVersion, | 	// If secretAnnotations is nil we create an empty map so we can later assign values for the OP Annotations in the map | ||||||
| 		ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID), | 	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 != "" { | 	if autoRestart != "" { | ||||||
| 		_, err := utils.StringToBool(autoRestart) | 		_, err := utils.StringToBool(autoRestart) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) | 			log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		annotations[RestartDeploymentsAnnotation] = autoRestart | 		secretAnnotations[RestartDeploymentsAnnotation] = autoRestart | ||||||
| 	} | 	} | ||||||
| 	secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, annotations, *item) |  | ||||||
|  | 	// "Opaque" and "" secret types are treated the same by Kubernetes. | ||||||
|  | 	secret := BuildKubernetesSecretFromOnePasswordItem(secretName, namespace, secretAnnotations, labels, secretType, *item) | ||||||
|  |  | ||||||
| 	currentSecret := &corev1.Secret{} | 	currentSecret := &corev1.Secret{} | ||||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) | 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) | ||||||
| @@ -49,9 +67,17 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if currentSecret.Annotations[VersionAnnotation] != itemVersion { | 	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)) | 		log.Info(fmt.Sprintf("Updating Secret %v at namespace '%v'", secret.Name, secret.Namespace)) | ||||||
| 		currentSecret.ObjectMeta.Annotations = annotations | 		currentSecret.ObjectMeta.Annotations = secretAnnotations | ||||||
|  | 		currentSecret.ObjectMeta.Labels = labels | ||||||
| 		currentSecret.Data = secret.Data | 		currentSecret.Data = secret.Data | ||||||
| 		return kubeClient.Update(context.Background(), currentSecret) | 		return kubeClient.Update(context.Background(), currentSecret) | ||||||
| 	} | 	} | ||||||
| @@ -60,14 +86,16 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, item onepassword.Item) *corev1.Secret { | func BuildKubernetesSecretFromOnePasswordItem(name, namespace string, annotations map[string]string, labels map[string]string, secretType string, item onepassword.Item) *corev1.Secret { | ||||||
| 	return &corev1.Secret{ | 	return &corev1.Secret{ | ||||||
| 		ObjectMeta: metav1.ObjectMeta{ | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
| 			Name:        name, | 			Name:        formatSecretName(name), | ||||||
| 			Namespace:   namespace, | 			Namespace:   namespace, | ||||||
| 			Annotations: annotations, | 			Annotations: annotations, | ||||||
|  | 			Labels:      labels, | ||||||
| 		}, | 		}, | ||||||
| 		Data: BuildKubernetesSecretData(item.Fields), | 		Data: BuildKubernetesSecretData(item.Fields), | ||||||
|  | 		Type: corev1.SecretType(secretType), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -75,8 +103,59 @@ func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byt | |||||||
| 	secretData := map[string][]byte{} | 	secretData := map[string][]byte{} | ||||||
| 	for i := 0; i < len(fields); i++ { | 	for i := 0; i < len(fields); i++ { | ||||||
| 		if fields[i].Value != "" { | 		if fields[i].Value != "" { | ||||||
| 			secretData[fields[i].Label] = []byte(fields[i].Value) | 			key := formatSecretDataName(fields[i].Label) | ||||||
|  | 			secretData[key] = []byte(fields[i].Value) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return secretData | 	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 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import ( | |||||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | 	"github.com/1Password/connect-sdk-go/onepassword" | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
|  | 	kubeValidate "k8s.io/apimachinery/pkg/util/validation" | ||||||
| 	"k8s.io/client-go/kubernetes" | 	"k8s.io/client-go/kubernetes" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||||
| ) | ) | ||||||
| @@ -30,7 +31,13 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
|  |  | ||||||
| 	kubeClient := fake.NewFakeClient() | 	kubeClient := fake.NewFakeClient() | ||||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation) | 	secretLabels := map[string]string{} | ||||||
|  | 	secretAnnotations := map[string]string{ | ||||||
|  | 		"testAnnotation": "exists", | ||||||
|  | 	} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
|  | 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Unexpected error: %v", err) | 		t.Errorf("Unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -42,6 +49,10 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 	compareFields(item.Fields, createdSecret.Data, t) | 	compareFields(item.Fields, createdSecret.Data, t) | ||||||
| 	compareAnnotationsToItem(createdSecret.Annotations, item, 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 TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||||
| @@ -55,7 +66,12 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
|  |  | ||||||
| 	kubeClient := fake.NewFakeClient() | 	kubeClient := fake.NewFakeClient() | ||||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation) | 	secretLabels := map[string]string{} | ||||||
|  | 	secretAnnotations := map[string]string{} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
|  | 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Unexpected error: %v", err) | 		t.Errorf("Unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -66,7 +82,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	newItem.Version = 456 | 	newItem.Version = 456 | ||||||
| 	newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | 	newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | ||||||
| 	newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" | 	newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||||
| 	err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation) | 	err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, secretAnnotations) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Unexpected error: %v", err) | 		t.Errorf("Unexpected error: %v", err) | ||||||
| 	} | 	} | ||||||
| @@ -99,9 +115,11 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 	item := onepassword.Item{} | 	item := onepassword.Item{} | ||||||
| 	item.Fields = generateFields(5) | 	item.Fields = generateFields(5) | ||||||
|  | 	labels := map[string]string{} | ||||||
|  | 	secretType := "" | ||||||
|  |  | ||||||
| 	kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item) | 	kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item) | ||||||
| 	if kubeSecret.Name != name { | 	if kubeSecret.Name != strings.ToLower(name) { | ||||||
| 		t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) | 		t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) | ||||||
| 	} | 	} | ||||||
| 	if kubeSecret.Namespace != namespace { | 	if kubeSecret.Namespace != namespace { | ||||||
| @@ -113,6 +131,79 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { | |||||||
| 	compareFields(item.Fields, kubeSecret.Data, t) | 	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) | ||||||
|  |  | ||||||
|  | 	// 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) | ||||||
|  | 	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) { | func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { | ||||||
| 	actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) | 	actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -164,3 +255,10 @@ func ParseVaultIdAndItemIdFromPath(path string) (string, string, error) { | |||||||
| 	} | 	} | ||||||
| 	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) | 	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 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package onepassword | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"os" | 	"os" | ||||||
|  |  | ||||||
| 	appsv1 "k8s.io/api/apps/v1" | 	appsv1 "k8s.io/api/apps/v1" | ||||||
| @@ -17,13 +18,13 @@ var logConnectSetup = logf.Log.WithName("ConnectSetup") | |||||||
| var deploymentPath = "deploy/connect/deployment.yaml" | var deploymentPath = "deploy/connect/deployment.yaml" | ||||||
| var servicePath = "deploy/connect/service.yaml" | var servicePath = "deploy/connect/service.yaml" | ||||||
|  |  | ||||||
| func SetupConnect(kubeClient client.Client) error { | func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { | ||||||
| 	err := setupService(kubeClient, servicePath) | 	err := setupService(kubeClient, servicePath, deploymentNamespace) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = setupDeployment(kubeClient, deploymentPath) | 	err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -31,22 +32,22 @@ func SetupConnect(kubeClient client.Client) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func setupDeployment(kubeClient client.Client, deploymentPath string) error { | func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { | ||||||
| 	existingDeployment := &appsv1.Deployment{} | 	existingDeployment := &appsv1.Deployment{} | ||||||
|  |  | ||||||
| 	// check if deployment has already been created | 	// check if deployment has already been created | ||||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingDeployment) | 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.IsNotFound(err) { | 		if errors.IsNotFound(err) { | ||||||
| 			logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") | 			logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") | ||||||
| 			return createDeployment(kubeClient, deploymentPath) | 			return createDeployment(kubeClient, deploymentPath, deploymentNamespace) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| func createDeployment(kubeClient client.Client, deploymentPath string) error { | func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { | ||||||
| 	deployment, err := getDeploymentToCreate(deploymentPath) | 	deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -59,12 +60,16 @@ func createDeployment(kubeClient client.Client, deploymentPath string) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) { | func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) { | ||||||
| 	f, err := os.Open(deploymentPath) | 	f, err := os.Open(deploymentPath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	deployment := &appsv1.Deployment{} | 	deployment := &appsv1.Deployment{ | ||||||
|  | 		ObjectMeta: v1.ObjectMeta{ | ||||||
|  | 			Namespace: deploymentNamespace, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) | 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -73,26 +78,30 @@ func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) { | |||||||
| 	return deployment, nil | 	return deployment, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func setupService(kubeClient client.Client, servicePath string) error { | func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { | ||||||
| 	existingService := &corev1.Service{} | 	existingService := &corev1.Service{} | ||||||
|  |  | ||||||
| 	//check if service has already been created | 	//check if service has already been created | ||||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingService) | 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.IsNotFound(err) { | 		if errors.IsNotFound(err) { | ||||||
| 			logConnectSetup.Info("No existing Connect service found. Creating Service") | 			logConnectSetup.Info("No existing Connect service found. Creating Service") | ||||||
| 			return createService(kubeClient, servicePath) | 			return createService(kubeClient, servicePath, deploymentNamespace) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  |  | ||||||
| func createService(kubeClient client.Client, servicePath string) error { | func createService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { | ||||||
| 	f, err := os.Open(servicePath) | 	f, err := os.Open(servicePath) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	service := &corev1.Service{} | 	service := &corev1.Service{ | ||||||
|  | 		ObjectMeta: v1.ObjectMeta{ | ||||||
|  | 			Namespace: deploymentNamespace, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) | 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ func TestServiceSetup(t *testing.T) { | |||||||
| 	// Create a fake client to mock API calls. | 	// Create a fake client to mock API calls. | ||||||
| 	client := fake.NewFakeClientWithScheme(s, objs...) | 	client := fake.NewFakeClientWithScheme(s, objs...) | ||||||
|  |  | ||||||
| 	err := setupService(client, "../../deploy/connect/service.yaml") | 	err := setupService(client, "../../deploy/connect/service.yaml", defaultNamespacedName.Namespace) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Error Setting Up Connect: %v", err) | 		t.Errorf("Error Setting Up Connect: %v", err) | ||||||
| @@ -50,7 +50,7 @@ func TestDeploymentSetup(t *testing.T) { | |||||||
| 	// Create a fake client to mock API calls. | 	// Create a fake client to mock API calls. | ||||||
| 	client := fake.NewFakeClientWithScheme(s, objs...) | 	client := fake.NewFakeClientWithScheme(s, objs...) | ||||||
|  |  | ||||||
| 	err := setupDeployment(client, "../../deploy/connect/deployment.yaml") | 	err := setupDeployment(client, "../../deploy/connect/deployment.yaml", defaultNamespacedName.Namespace) | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Errorf("Error Setting Up Connect: %v", err) | 		t.Errorf("Error Setting Up Connect: %v", err) | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| package onepassword | package onepassword | ||||||
|  |  | ||||||
| import corev1 "k8s.io/api/core/v1" | import ( | ||||||
|  | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
| func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool { | func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string]*corev1.Secret) bool { | ||||||
| 	for i := 0; i < len(containers); i++ { | 	for i := 0; i < len(containers); i++ { | ||||||
| @@ -13,6 +15,15 @@ func AreContainersUsingSecrets(containers []corev1.Container, secrets map[string | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		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 | 	return false | ||||||
| } | } | ||||||
| @@ -28,6 +39,15 @@ func AppendUpdatedContainerSecrets(containers []corev1.Container, secrets map[st | |||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		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 | 	return updatedDeploymentSecrets | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,9 +4,10 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	corev1 "k8s.io/api/core/v1" | 	corev1 "k8s.io/api/core/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestAreContainersUsingSecrets(t *testing.T) { | func TestAreContainersUsingSecretsFromEnv(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]*corev1.Secret{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": &corev1.Secret{}, | 		"onepassword-database-secret": &corev1.Secret{}, | ||||||
| 		"onepassword-api-key":         &corev1.Secret{}, | 		"onepassword-api-key":         &corev1.Secret{}, | ||||||
| @@ -18,7 +19,26 @@ func TestAreContainersUsingSecrets(t *testing.T) { | |||||||
| 		"some_other_key", | 		"some_other_key", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containers := generateContainers(containerSecretNames) | 	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) { | 	if !AreContainersUsingSecrets(containers, secretNamesToSearch) { | ||||||
| 		t.Errorf("Expected that containers were using secrets but they were not detected.") | 		t.Errorf("Expected that containers were using secrets but they were not detected.") | ||||||
| @@ -27,17 +47,39 @@ func TestAreContainersUsingSecrets(t *testing.T) { | |||||||
|  |  | ||||||
| func TestAreContainersNotUsingSecrets(t *testing.T) { | func TestAreContainersNotUsingSecrets(t *testing.T) { | ||||||
| 	secretNamesToSearch := map[string]*corev1.Secret{ | 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||||
| 		"onepassword-database-secret": &corev1.Secret{}, | 		"onepassword-database-secret": {}, | ||||||
| 		"onepassword-api-key":         &corev1.Secret{}, | 		"onepassword-api-key":         {}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containerSecretNames := []string{ | 	containerSecretNames := []string{ | ||||||
| 		"some_other_key", | 		"some_other_key", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	containers := generateContainers(containerSecretNames) | 	containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) | ||||||
|  |  | ||||||
| 	if AreContainersUsingSecrets(containers, secretNamesToSearch) { | 	if AreContainersUsingSecrets(containers, secretNamesToSearch) { | ||||||
| 		t.Errorf("Expected that containers were not using secrets but they were detected.") | 		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.") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	deployment := &appsv1.Deployment{} | 	deployment := &appsv1.Deployment{} | ||||||
| 	deployment.Spec.Template.Spec.Containers = generateContainers(containerSecretNames) | 	deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames) | ||||||
| 	if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { | 	if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { | ||||||
| 		t.Errorf("Expected that deployment was using secrets but they were not detected.") | 		t.Errorf("Expected that deployment was using secrets but they were not detected.") | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -17,8 +17,7 @@ func generateVolumes(names []string) []corev1.Volume { | |||||||
| 	} | 	} | ||||||
| 	return volumes | 	return volumes | ||||||
| } | } | ||||||
|  | func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container { | ||||||
| func generateContainers(names []string) []corev1.Container { |  | ||||||
| 	containers := []corev1.Container{} | 	containers := []corev1.Container{} | ||||||
| 	for i := 0; i < len(names); i++ { | 	for i := 0; i < len(names); i++ { | ||||||
| 		container := corev1.Container{ | 		container := corev1.Container{ | ||||||
| @@ -40,3 +39,16 @@ func generateContainers(names []string) []corev1.Container { | |||||||
| 	} | 	} | ||||||
| 	return containers | 	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 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -118,7 +118,8 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]* | |||||||
|  |  | ||||||
| 		item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation]) | 		item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation]) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("Failed to retrieve item: %v", err) | 			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) | 		itemVersion := fmt.Sprint(item.Version) | ||||||
| @@ -131,7 +132,7 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]* | |||||||
| 			} | 			} | ||||||
| 			log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) | 			log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) | ||||||
| 			secret.Annotations[VersionAnnotation] = itemVersion | 			secret.Annotations[VersionAnnotation] = itemVersion | ||||||
| 			updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item) | 			updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, secret.Labels, string(secret.Type), *item) | ||||||
| 			h.client.Update(context.Background(), updatedSecret) | 			h.client.Update(context.Background(), updatedSecret) | ||||||
| 			if updatedSecrets[secret.Namespace] == nil { | 			if updatedSecrets[secret.Namespace] == nil { | ||||||
| 				updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) | 				updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user