mirror of
				https://github.com/1Password/onepassword-operator.git
				synced 2025-10-31 03:39:39 +00:00 
			
		
		
		
	Compare commits
	
		
			116 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 0796b9c5e2 | ||
|   | 37a0f4b51e | ||
|   | 004e0101ff | ||
|   | 6326a856ae | ||
|   | 1ddf92c5a0 | ||
|   | f5c6fa5860 | ||
|   | afa076d321 | ||
|   | d4b04c233c | ||
|   | ea68cfc2b4 | ||
|   | 58b4ff8ecf | ||
|   | d93fecdc76 | ||
|   | 486465247d | ||
|   | 79868ae374 | ||
|   | 6286f7e306 | ||
|   | 0b5efc8690 | ||
|   | c00baeedcb | ||
|   | a37bddbfd9 | ||
|   | bd9922f635 | ||
|   | 8fa4413880 | ||
|   | 62e55a3f19 | ||
|   | d6f7b80c40 | ||
|   | a903f9b1af | ||
|   | 5cddc9d8a9 | ||
|   | 7e1b94fae7 | ||
|   | 6953a89c89 | ||
|   | 0d9e07f543 | ||
|   | 098d504d2a | ||
|   | b68d9a5d79 | ||
|   | befcaae457 | ||
|   | b24aa48bd6 | ||
|   | b1e251dee6 | ||
|   | a34c6e8b38 | ||
|   | b16960057a | ||
|   | 285496dc7e | ||
|   | f38cf7e1c2 | ||
|   | bb7a0c8ca9 | ||
|   | 302653832e | ||
|   | a1bcfdfdcb | ||
|   | c0f1632638 | ||
|   | c46065fa7a | ||
|   | 5d229c42d5 | ||
|   | c7235b4f09 | ||
|   | 5183fc129a | ||
|   | 7d619165b2 | ||
|   | 0363ae1e4e | ||
|   | d9e003bdb7 | ||
|   | b25f943b3a | ||
|   | 5fab662424 | ||
|   | d807e92c36 | ||
|   | 244771717c | ||
|   | a760e524ea | ||
|   | 7aeb36e383 | ||
|   | 5c2f840623 | ||
|   | 670040477e | ||
|   | a45a310611 | ||
|   | d80e8dd799 | ||
|   | 88728909ff | ||
|   | e365ebfdfa | ||
|   | 2c4b4df01a | ||
|   | 49d984c6f2 | ||
|   | 72cad7284c | ||
|   | 19f774bb2d | ||
|   | 0193a98681 | ||
|   | f241d7423d | ||
|   | 6043e0da0b | ||
|   | 753cc5e9a3 | ||
|   | 8cfe98073e | ||
|   | c0037526b0 | ||
|   | 96b42e7c52 | ||
|   | 579b5848da | ||
|   | dff934cbc3 | ||
|   | 2096f4440f | ||
|   | b3fc707337 | ||
|   | 32643651d9 | ||
|   | ba8d3fa698 | ||
|   | c57aa22a9c | ||
|   | 48944b0d56 | ||
|   | 313cd1169b | ||
|   | b50d864b50 | ||
|   | 1643385d9b | ||
|   | 9441214733 | ||
|   | 7e4e988813 | ||
|   | fb1262f1bd | ||
|   | 68f084080e | ||
|   | a428fe7462 | ||
|   | ea2d1f8a09 | ||
|   | bd96d50a9b | ||
|   | 859c9e3462 | ||
|   | 9dabac4a55 | ||
|   | d927a08790 | ||
|   | 933f7c4e2c | ||
|   | 81eb9a521f | ||
|   | eb32bd7f94 | ||
|   | a5781af949 | ||
|   | 0aa5781acd | ||
|   | 700be4426f | ||
|   | 76ef9aa372 | ||
|   | d7e6704314 | ||
|   | 2443979602 | ||
|   | 5b65196d31 | ||
|   | e7df8a485d | ||
|   | ded76138da | ||
|   | a5db6aeb81 | ||
|   | d45f682c37 | ||
|   | d0c1235e58 | ||
|   | 9e8f621020 | ||
|   | 8dd7a28456 | ||
|   | 43b06dd7aa | ||
|   | e8e01d6578 | ||
|   | b53e017b77 | ||
|   | b2565cebf8 | ||
|   | 9459d2e292 | ||
|   | 0409b17ef4 | ||
|   | 2e47b76d4c | ||
|   | 1cca09df90 | ||
|   | b9cb92eb1b | 
							
								
								
									
										36
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| --- | ||||
| name: Bug report | ||||
| about: Report bugs and errors found while using the Operator. | ||||
| title: '' | ||||
| labels: bug | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### Your environment | ||||
|  | ||||
| <!-- Version of the Operator when the error occurred --> | ||||
| Operator Version: | ||||
|  | ||||
| <!-- What version of the Connect server are you running? | ||||
| You can get this information from the Integrations section in 1Password | ||||
| https://start.1password.com/integrations/active | ||||
| --> | ||||
| Connect Server Version: | ||||
|  | ||||
| <!-- What version of Kubernetes have you deployed the operator to? --> | ||||
| Kubernetes Version: | ||||
|  | ||||
| ## What happened? | ||||
| <!-- Describe the bug or error --> | ||||
|  | ||||
| ## What did you expect to happen? | ||||
| <!-- Describe what should have happened --> | ||||
|  | ||||
| ## Steps to reproduce | ||||
| 1. <!-- Describe Steps to reproduce the issue --> | ||||
|  | ||||
|  | ||||
| ## Notes & Logs | ||||
| <!-- Paste any logs here that may help with debugging. | ||||
| Remember to remove any sensitive information before sharing! --> | ||||
							
								
								
									
										9
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| # docs: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser | ||||
| blank_issues_enabled: true | ||||
| contact_links: | ||||
|   - name: 1Password Community | ||||
|     url: https://1password.community/categories/secrets-automation | ||||
|     about: Please ask general Secrets Automation questions here. | ||||
|   - name: 1Password Security Bug Bounty | ||||
|     url: https://bugcrowd.com/agilebits | ||||
|     about: Please report security vulnerabilities here. | ||||
							
								
								
									
										32
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| --- | ||||
| name: Feature request | ||||
| about: Suggest an idea for the Operator | ||||
| title: '' | ||||
| labels: feature-request | ||||
| assignees: '' | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### Summary | ||||
| <!-- Briefly describe the feature in one or two sentences. You can include more details later. --> | ||||
|  | ||||
| ### Use cases | ||||
| <!-- Describe the use cases that make this feature useful to others. | ||||
| The description should help the reader understand why the feature is necessary. | ||||
| The better we understand your use case, the better we can help create an appropriate solution. --> | ||||
|  | ||||
| ### Proposed solution | ||||
| <!-- If you already have an idea for how the feature should work, use this space to describe it. | ||||
| We'll work with you to find a workable approach, and any implementation details are appreciated. | ||||
| --> | ||||
|  | ||||
| ### Is there a workaround to accomplish this today? | ||||
| <!-- If there's a way to accomplish this feature request without changes to the codebase, we'd like to hear it. | ||||
| --> | ||||
|  | ||||
| ### References & Prior Work | ||||
| <!-- If a similar feature was implemented in another project or tool, add a link so we can better understand your request. | ||||
| Links to relevant documentation or RFCs are also appreciated. --> | ||||
|  | ||||
| * <!-- Reference 1 --> | ||||
| * <!-- Reference 2, etc --> | ||||
							
								
								
									
										11
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,20 +1,11 @@ | ||||
| name: Build and Test | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - '**' | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - $default-branch | ||||
| on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|  | ||||
|   build: | ||||
|     name: Build | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|  | ||||
|     - name: Set up Go 1.x | ||||
|       uses: actions/setup-go@v2 | ||||
|       with: | ||||
|   | ||||
							
								
								
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| name: release | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     tags: | ||||
|       - 'v*' | ||||
|  | ||||
| jobs: | ||||
|   release-docker: | ||||
|     runs-on: ubuntu-latest | ||||
|     env: | ||||
|       DOCKER_CLI_EXPERIMENTAL: "enabled" | ||||
|     steps: | ||||
|       - | ||||
|         name: Checkout | ||||
|         uses: actions/checkout@v2 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|       - | ||||
|         name: Docker meta | ||||
|         id: meta | ||||
|         uses: crazy-max/ghaction-docker-meta@v2 | ||||
|         with: | ||||
|           images: | | ||||
|             1password/onepassword-operator | ||||
|           # Publish image for x.y.z and x.y | ||||
|           # The latest tag is automatically added for semver tags | ||||
|           tags: | | ||||
|             type=semver,pattern={{version}} | ||||
|             type=semver,pattern={{major}}.{{minor}} | ||||
|       - name: Get the version from tag | ||||
|         id: get_version | ||||
|         run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v} | ||||
|       - | ||||
|         name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v1 | ||||
|       - | ||||
|         name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v1 | ||||
|       - | ||||
|         name: Docker Login | ||||
|         uses: docker/login-action@v1 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||
|           password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||
|       - | ||||
|         name: Build and push | ||||
|         uses: docker/build-push-action@v2 | ||||
|         with: | ||||
|           context: . | ||||
|           file: Dockerfile | ||||
|           platforms: linux/amd64,linux/arm64,linux/arm/v7 | ||||
|           push: true | ||||
|           tags: ${{ steps.meta.outputs.tags }} | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           build-args: | | ||||
|             operator_version=${{ steps.get_version.outputs.VERSION }} | ||||
							
								
								
									
										119
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										119
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -12,43 +12,124 @@ | ||||
|  | ||||
| --- | ||||
|  | ||||
| [//]: # (START/v1.0.0) | ||||
| [//]: # "START/v1.4.1" | ||||
|  | ||||
| # v1.4.1 | ||||
|  | ||||
| ## Fixes | ||||
|  | ||||
| - OwnerReferences on secrets are now persisted after an item is updated. {#101} | ||||
| - Annotations from a Deployment or OnePasswordItem are no longer applied to Secrets that are created for it. {#102} | ||||
|  | ||||
| --- | ||||
|  | ||||
| [//]: # "START/v1.4.0" | ||||
|  | ||||
| # v1.4.0 | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - The operator now declares the an OwnerReference for the secrets it manages. This should stop secrets from getting pruned by tools like Argo CD. {#51,#84,#96} | ||||
|  | ||||
| --- | ||||
|  | ||||
| [//]: # "START/v1.3.0" | ||||
|  | ||||
| # v1.3.0 | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - Added support for loading secrets from files stored in 1Password. {#47} | ||||
|  | ||||
| --- | ||||
|  | ||||
| [//]: # "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" | ||||
|  | ||||
| # v1.0.1 | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| - This release also contains an arm64 Docker image. {#20} | ||||
| - Docker images are also pushed to the :latest and :<major>.<minor> tags. | ||||
|  | ||||
| --- | ||||
|  | ||||
| [//]: # "START/v1.0.0" | ||||
|  | ||||
| # v1.0.0 | ||||
|  | ||||
| ## Features: | ||||
| * Option to automatically deploy 1Password Connect via the operator | ||||
| * Ignore restart annotation when looking for 1Password annotations | ||||
| * Release Automation | ||||
| * Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource | ||||
| * Adding configuration for auto rolling restart on deployments | ||||
| * Configure Auto Restarts for a OnePasswordItem Custom Resource | ||||
| * Update Connect Dependencies to latest | ||||
| * Add Github action for building and testing operator | ||||
|  | ||||
| - Option to automatically deploy 1Password Connect via the operator | ||||
| - Ignore restart annotation when looking for 1Password annotations | ||||
| - Release Automation | ||||
| - Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource | ||||
| - Adding configuration for auto rolling restart on deployments | ||||
| - Configure Auto Restarts for a OnePasswordItem Custom Resource | ||||
| - Update Connect Dependencies to latest | ||||
| - Add Github action for building and testing operator | ||||
|  | ||||
| ## Fixes: | ||||
| * Fix spec field example for OnePasswordItem in readme | ||||
| * Casing of annotations are now consistent | ||||
|  | ||||
| - Fix spec field example for OnePasswordItem in readme | ||||
| - Casing of annotations are now consistent | ||||
|  | ||||
| --- | ||||
|  | ||||
| [//]: # (START/v0.0.2) | ||||
| [//]: # "START/v0.0.2" | ||||
|  | ||||
| # v0.0.2 | ||||
|  | ||||
| ## Features: | ||||
| * Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>` | ||||
|  | ||||
| - Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>` | ||||
|  | ||||
| --- | ||||
|  | ||||
| [//]: # (START/v0.0.1) | ||||
| [//]: # "START/v0.0.1" | ||||
|  | ||||
| # v0.0.1 | ||||
|  | ||||
| Initial 1Password Operator release | ||||
|  | ||||
| ## Features | ||||
| * watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret | ||||
| * watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets | ||||
| * watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes | ||||
| * restart pods when secret has been updated | ||||
| * cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted | ||||
|  | ||||
| - watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret | ||||
| - watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets | ||||
| - watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes | ||||
| - restart pods when secret has been updated | ||||
| - cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted | ||||
|  | ||||
| --- | ||||
|   | ||||
| @@ -14,11 +14,9 @@ COPY vendor/ vendor/ | ||||
| # Build | ||||
| ARG operator_version=dev | ||||
| RUN CGO_ENABLED=0 \ | ||||
|     GOOS=linux \ | ||||
|     GOARCH=amd64 \ | ||||
|     GO111MODULE=on \ | ||||
|     go build \ | ||||
|     -ldflags "-X version.Version=$operator_version" \ | ||||
|     -ldflags "-X \"github.com/1Password/onepassword-operator/version.Version=$operator_version\"" \ | ||||
|     -mod vendor \ | ||||
|     -a -o manager main.go | ||||
|  | ||||
|   | ||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							| @@ -20,12 +20,12 @@ test/coverage:	## Run test suite with coverage report | ||||
| 	go test -v ./... -cover | ||||
|  | ||||
| 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 "Tag: $(DOCKER_IMG_TAG)" | ||||
|  | ||||
| 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 | ||||
| 	@mkdir -p dist | ||||
|   | ||||
							
								
								
									
										62
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								README.md
									
									
									
									
									
								
							| @@ -13,8 +13,8 @@ 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/) | ||||
| - [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. | ||||
| - [Generated a 1password-credentials.json file and issued a 1Password Connect API Token for the K8s Operator integration](https://support.1password.com/secrets-automation/) | ||||
| - [1Password Connect deployed to Kubernetes](https://support.1password.com/connect-deploy-kubernetes/#step-2-deploy-a-1password-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 | ||||
|  | ||||
| @@ -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: | ||||
|  | ||||
| ```bash | ||||
| $ cat 1password-credentials.json | base64 | \ | ||||
| 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=1password-credentials.json | ||||
| kubectl create secret generic op-credentials --from-file=op-session | ||||
| ``` | ||||
|  | ||||
| Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`: | ||||
| @@ -53,28 +52,28 @@ 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:  | ||||
|  | ||||
| ```bash | ||||
| $ kubectl create secret generic op-operator-connect-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: | ||||
| ```bash | ||||
| $ kubectl create secret generic op-operator-connect-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/cs/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) | ||||
|  | ||||
| **Set Permissions For Operator** | ||||
|  | ||||
| We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder. | ||||
|  | ||||
| ```bash | ||||
| $ kubectl apply -f deploy/permissions.yaml | ||||
| kubectl apply -f deploy/permissions.yaml | ||||
| ``` | ||||
|  | ||||
| **Create Custom One Password Secret Resource** | ||||
|  | ||||
| ```bash | ||||
| $ kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml | ||||
| kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml | ||||
| ``` | ||||
|  | ||||
| **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: | ||||
|  | ||||
| - **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. | ||||
| - **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. | ||||
| - **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. | ||||
|  | ||||
| @@ -102,7 +101,7 @@ To create a Kubernetes Secret from a 1Password item, create a yaml file with the | ||||
|  | ||||
| ```yaml | ||||
| apiVersion: onepassword.com/v1 | ||||
| kind: OnePasswordItem # {insert_new_name} | ||||
| kind: OnePasswordItem | ||||
| metadata: | ||||
|   name: <item_name> #this name will also be used for naming the generated kubernetes secret | ||||
| spec: | ||||
| @@ -112,13 +111,13 @@ spec: | ||||
| Deploy the OnePasswordItem to Kubernetes: | ||||
|  | ||||
| ```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: | ||||
|  | ||||
| ```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. | ||||
| @@ -131,12 +130,17 @@ kind: Deployment | ||||
| metadata: | ||||
|   name: deployment-example | ||||
|   annotations: | ||||
|     operator.1password.io/item-path: "vaults/{vault_id_or_title}/items/{item_id_or_title}" | ||||
|     operator.1password.io/item-name: "{secret_name}" | ||||
|     operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>" | ||||
|     operator.1password.io/item-name: "<secret_name>" | ||||
| ``` | ||||
|  | ||||
| Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path. | ||||
|  | ||||
| The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.  | ||||
| In case of fields that store files, the file's contents will be used as the value. | ||||
|  | ||||
| Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence. | ||||
|  | ||||
| Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret. | ||||
|  | ||||
| If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes. | ||||
| @@ -144,7 +148,12 @@ If a 1Password Item that is linked to a Kubernetes Secret is updated within the | ||||
| --- | ||||
| **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. | ||||
|  | ||||
| --- | ||||
|  | ||||
| @@ -163,9 +172,10 @@ apiVersion: v1 | ||||
| kind: Namespace | ||||
| metadata: | ||||
|   name: "example-namespace" | ||||
|   operator.1password.io/auto-restart: "true" | ||||
|   annotations: | ||||
|     operator.1password.io/auto-restart: "true" | ||||
| ``` | ||||
| If the value is not set, the auto reset settings on the operator will be used. This value can be overwritten by deployment. | ||||
| If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment. | ||||
|  | ||||
| **Per Deployment** | ||||
| This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below: | ||||
| @@ -175,9 +185,10 @@ apiVersion: v1 | ||||
| kind: Deployment | ||||
| metadata: | ||||
|   name: "example-deployment" | ||||
|   operator.1password.io/auto-restart: "true" | ||||
|   annotations: | ||||
|     operator.1password.io/auto-restart: "true" | ||||
| ``` | ||||
| If the value is not set, the auto reset settings on the namespace will be used. | ||||
| If the value is not set, the auto restart settings on the namespace will be used. | ||||
|  | ||||
| **Per OnePasswordItem Custom Resource** | ||||
| This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below: | ||||
| @@ -187,9 +198,10 @@ apiVersion: onepassword.com/v1 | ||||
| kind: OnePasswordItem | ||||
| metadata: | ||||
|   name: example | ||||
|   operator.1password.io/auto-restart: "true" | ||||
|   annotations: | ||||
|     operator.1password.io/auto-restart: "true" | ||||
| ``` | ||||
| If the value is not set, the auto reset settings on the deployment will be used. | ||||
| If the value is not set, the auto restart settings on the deployment will be used. | ||||
|  | ||||
| ## Development | ||||
|  | ||||
| @@ -224,4 +236,4 @@ make test/coverage | ||||
|  | ||||
| Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).  | ||||
|  | ||||
| For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits). | ||||
| For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits). | ||||
|   | ||||
| @@ -83,9 +83,11 @@ func main() { | ||||
|  | ||||
| 	printVersion() | ||||
|  | ||||
| 	namespace, err := k8sutil.GetWatchNamespace() | ||||
| 	namespace := os.Getenv(k8sutil.WatchNamespaceEnvVar) | ||||
|  | ||||
| 	deploymentNamespace, err := k8sutil.GetOperatorNamespace() | ||||
| 	if err != nil { | ||||
| 		log.Error(err, "Failed to get watch namespace") | ||||
| 		log.Error(err, "Failed to get namespace") | ||||
| 		os.Exit(1) | ||||
| 	} | ||||
|  | ||||
| @@ -139,7 +141,7 @@ func main() { | ||||
| 		go func() { | ||||
| 			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. | ||||
| 				if err != nil && !errors.Is(err, &cache.ErrCacheNotStarted{}) { | ||||
| 					log.Error(err, "") | ||||
| @@ -176,7 +178,10 @@ func main() { | ||||
| 				ticker.Stop() | ||||
| 				return | ||||
| 			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 | ||||
| metadata: | ||||
|   name: onepassword-connect | ||||
|   namespace: default | ||||
| spec: | ||||
|   selector: | ||||
|     matchLabels: | ||||
|   | ||||
| @@ -2,7 +2,6 @@ apiVersion: v1 | ||||
| kind: Service | ||||
| metadata: | ||||
|   name: onepassword-connect | ||||
|   namespace: default | ||||
| spec: | ||||
|   type: NodePort | ||||
|   selector: | ||||
|   | ||||
| @@ -39,4 +39,7 @@ spec: | ||||
|           status: | ||||
|             description: OnePasswordItemStatus defines the observed state of OnePasswordItem | ||||
|             type: object | ||||
|           type: | ||||
|             description: 'Kubernetes secret type. More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types' | ||||
|             type: string | ||||
|         type: object | ||||
|   | ||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,11 +3,11 @@ module github.com/1Password/onepassword-operator | ||||
| go 1.13 | ||||
|  | ||||
| require ( | ||||
| 	github.com/1Password/connect-sdk-go v1.0.1 | ||||
| 	github.com/1Password/connect-sdk-go v1.2.0 | ||||
| 	github.com/operator-framework/operator-sdk v0.19.0 | ||||
| 	github.com/prometheus/common v0.14.0 // indirect | ||||
| 	github.com/spf13/pflag v1.0.5 | ||||
| 	github.com/stretchr/testify v1.6.1 | ||||
| 	github.com/stretchr/testify v1.7.0 | ||||
| 	k8s.io/api v0.18.2 | ||||
| 	k8s.io/apimachinery v0.18.2 | ||||
| 	k8s.io/client-go v12.0.0+incompatible | ||||
|   | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -20,6 +20,8 @@ contrib.go.opencensus.io/exporter/ocagent v0.6.0/go.mod h1:zmKjrJcdo0aYcVS7bmEeS | ||||
| dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= | ||||
| github.com/1Password/connect-sdk-go v1.0.1 h1:BOeMIxVk6/ISmLNWUkSxEbVI7tNr5+aNXIobMM0/I0U= | ||||
| github.com/1Password/connect-sdk-go v1.0.1/go.mod h1:br2BWk2sqgJFnOFK5WSDfBBmwQ6E7hV9LoPqrtHGRNY= | ||||
| github.com/1Password/connect-sdk-go v1.2.0 h1:WbIvmbDUpA89nyH0l3LF2iRSFJAv86d2D7IjVNjw6iw= | ||||
| github.com/1Password/connect-sdk-go v1.2.0/go.mod h1:qK2bF/GweAq812xj+HGfbauaE6cKX1MXfKhpAvoHEq8= | ||||
| github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= | ||||
| github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= | ||||
| github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= | ||||
| @@ -885,6 +887,8 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H | ||||
| github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= | ||||
| github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= | ||||
| github.com/thanos-io/thanos v0.11.0/go.mod h1:N/Yes7J68KqvmY+xM6J5CJqEvWIvKSR5sqGtmuD6wDc= | ||||
| github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= | ||||
|   | ||||
| @@ -26,6 +26,7 @@ type OnePasswordItemStatus struct { | ||||
| type OnePasswordItem struct { | ||||
| 	metav1.TypeMeta   `json:",inline"` | ||||
| 	metav1.ObjectMeta `json:"metadata,omitempty"` | ||||
| 	Type              string `json:"type,omitempty"` | ||||
|  | ||||
| 	Spec   OnePasswordItemSpec   `json:"spec,omitempty"` | ||||
| 	Status OnePasswordItemStatus `json:"status,omitempty"` | ||||
|   | ||||
| @@ -14,9 +14,11 @@ import ( | ||||
| 	appsv1 "k8s.io/api/apps/v1" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/errors" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	ctrl "sigs.k8s.io/controller-runtime" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/controller" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/handler" | ||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||
| @@ -114,7 +116,7 @@ func (r *ReconcileDeployment) Reconcile(request reconcile.Request) (reconcile.Re | ||||
| 			} | ||||
| 		} | ||||
| 		// Handles creation or updating secrets for deployment if needed | ||||
| 		if err := r.HandleApplyingDeployment(deployment.Namespace, annotations, request); err != nil { | ||||
| 		if err := r.HandleApplyingDeployment(deployment, deployment.Namespace, annotations, request); err != nil { | ||||
| 			return reconcile.Result{}, err | ||||
| 		} | ||||
| 		return reconcile.Result{}, nil | ||||
| @@ -187,10 +189,13 @@ func (r *ReconcileDeployment) removeOnePasswordFinalizerFromDeployment(deploymen | ||||
| 	return r.kubeClient.Update(context.Background(), deployment) | ||||
| } | ||||
|  | ||||
| func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotations map[string]string, request reconcile.Request) error { | ||||
| func (r *ReconcileDeployment) HandleApplyingDeployment(deployment *appsv1.Deployment, namespace string, annotations map[string]string, request reconcile.Request) error { | ||||
| 	reqLog := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) | ||||
|  | ||||
| 	secretName := annotations[op.NameAnnotation] | ||||
| 	secretLabels := map[string]string(nil) | ||||
| 	secretType := "" | ||||
|  | ||||
| 	if len(secretName) == 0 { | ||||
| 		reqLog.Info("No 'item-name' annotation set. 'item-path' and 'item-name' must be set as annotations to add new secret.") | ||||
| 		return nil | ||||
| @@ -201,5 +206,17 @@ func (r *ReconcileDeployment) HandleApplyingDeployment(namespace string, annotat | ||||
| 		return fmt.Errorf("Failed to retrieve item: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation]) | ||||
| 	// Create owner reference. | ||||
| 	gvk, err := apiutil.GVKForObject(deployment, r.scheme) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not to retrieve group version kind: %v", err) | ||||
| 	} | ||||
| 	ownerRef := &metav1.OwnerReference{ | ||||
| 		APIVersion: gvk.GroupVersion().String(), | ||||
| 		Kind:       gvk.Kind, | ||||
| 		Name:       deployment.GetName(), | ||||
| 		UID:        deployment.GetUID(), | ||||
| 	} | ||||
|  | ||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, namespace, item, annotations[op.RestartDeploymentsAnnotation], secretLabels, secretType, ownerRef) | ||||
| } | ||||
|   | ||||
| @@ -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{ | ||||
| 			TypeMeta: metav1.TypeMeta{ | ||||
| 				Kind:       deploymentKind, | ||||
| @@ -271,6 +271,7 @@ var tests = []testReconcileItem{ | ||||
| 					op.ItemPathAnnotation: itemPath, | ||||
| 					op.NameAnnotation:     name, | ||||
| 				}, | ||||
| 				Labels: map[string]string{}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		existingSecret: &corev1.Secret{ | ||||
| @@ -278,7 +279,8 @@ var tests = []testReconcileItem{ | ||||
| 				Name:      name, | ||||
| 				Namespace: namespace, | ||||
| 				Annotations: map[string]string{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | ||||
| 					op.ItemPathAnnotation: itemPath, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Data: expectedSecretData, | ||||
| @@ -289,8 +291,10 @@ var tests = []testReconcileItem{ | ||||
| 				Name:      name, | ||||
| 				Namespace: namespace, | ||||
| 				Annotations: map[string]string{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | ||||
| 					op.ItemPathAnnotation: itemPath, | ||||
| 				}, | ||||
| 				Labels: map[string]string(nil), | ||||
| 			}, | ||||
| 			Data: expectedSecretData, | ||||
| 		}, | ||||
| @@ -323,6 +327,7 @@ var tests = []testReconcileItem{ | ||||
| 					op.VersionAnnotation: "456", | ||||
| 				}, | ||||
| 			}, | ||||
| 			Type: corev1.SecretType(""), | ||||
| 			Data: expectedSecretData, | ||||
| 		}, | ||||
| 		expectedError: nil, | ||||
| @@ -334,6 +339,7 @@ var tests = []testReconcileItem{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 				}, | ||||
| 			}, | ||||
| 			Type: corev1.SecretType(""), | ||||
| 			Data: expectedSecretData, | ||||
| 		}, | ||||
| 		opItem: map[string]string{ | ||||
| @@ -367,6 +373,7 @@ var tests = []testReconcileItem{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 				}, | ||||
| 			}, | ||||
| 			Type: corev1.SecretType(""), | ||||
| 			Data: expectedSecretData, | ||||
| 		}, | ||||
| 		opItem: map[string]string{ | ||||
| @@ -376,7 +383,7 @@ var tests = []testReconcileItem{ | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| func TestReconcileDepoyment(t *testing.T) { | ||||
| func TestReconcileDeployment(t *testing.T) { | ||||
| 	for _, testData := range tests { | ||||
| 		t.Run(testData.testName, func(t *testing.T) { | ||||
|  | ||||
|   | ||||
| @@ -14,9 +14,11 @@ import ( | ||||
|  | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/errors" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/runtime" | ||||
| 	ctrl "sigs.k8s.io/controller-runtime" | ||||
| 	kubeClient "sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/controller" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/handler" | ||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||
| @@ -144,6 +146,8 @@ func (r *ReconcileOnePasswordItem) removeOnePasswordFinalizerFromOnePasswordItem | ||||
|  | ||||
| func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1.OnePasswordItem, request reconcile.Request) error { | ||||
| 	secretName := resource.GetName() | ||||
| 	labels := resource.Labels | ||||
| 	secretType := resource.Type | ||||
| 	autoRestart := resource.Annotations[op.RestartDeploymentsAnnotation] | ||||
|  | ||||
| 	item, err := onepassword.GetOnePasswordItemByPath(r.opConnectClient, resource.Spec.ItemPath) | ||||
| @@ -151,5 +155,17 @@ func (r *ReconcileOnePasswordItem) HandleOnePasswordItem(resource *onepasswordv1 | ||||
| 		return fmt.Errorf("Failed to retrieve item: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart) | ||||
| 	// Create owner reference. | ||||
| 	gvk, err := apiutil.GVKForObject(resource, r.scheme) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("could not to retrieve group version kind: %v", err) | ||||
| 	} | ||||
| 	ownerRef := &metav1.OwnerReference{ | ||||
| 		APIVersion: gvk.GroupVersion().String(), | ||||
| 		Kind:       gvk.Kind, | ||||
| 		Name:       resource.GetName(), | ||||
| 		UID:        resource.GetUID(), | ||||
| 	} | ||||
|  | ||||
| 	return kubeSecrets.CreateKubernetesSecretFromItem(r.kubeClient, secretName, resource.Namespace, item, autoRestart, labels, secretType, ownerRef) | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/1Password/onepassword-operator/pkg/kubernetessecrets" | ||||
| 	"github.com/1Password/onepassword-operator/pkg/mocks" | ||||
| 	op "github.com/1Password/onepassword-operator/pkg/onepassword" | ||||
|  | ||||
| @@ -31,6 +32,9 @@ const ( | ||||
| 	itemId                    = "nwrhuano7bcwddcviubpp4mhfq" | ||||
| 	username                  = "test-user" | ||||
| 	password                  = "QmHumKc$mUeEem7caHtbaBaJ" | ||||
| 	firstHost                 = "http://localhost:8080" | ||||
| 	awsKey                    = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" | ||||
| 	iceCream                  = "freezing blue 20%" | ||||
| 	userKey                   = "username" | ||||
| 	passKey                   = "password" | ||||
| 	version                   = 123 | ||||
| @@ -97,7 +101,7 @@ var tests = []testReconcileItem{ | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		testName: "Test Do not update if OnePassword Version has not changed", | ||||
| 		testName: "Test Do not update if OnePassword Version or VaultPath has not changed", | ||||
| 		customResource: &onepasswordv1.OnePasswordItem{ | ||||
| 			TypeMeta: metav1.TypeMeta{ | ||||
| 				Kind:       onePasswordItemKind, | ||||
| @@ -116,7 +120,8 @@ var tests = []testReconcileItem{ | ||||
| 				Name:      name, | ||||
| 				Namespace: namespace, | ||||
| 				Annotations: map[string]string{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | ||||
| 					op.ItemPathAnnotation: itemPath, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Data: expectedSecretData, | ||||
| @@ -127,7 +132,8 @@ var tests = []testReconcileItem{ | ||||
| 				Name:      name, | ||||
| 				Namespace: namespace, | ||||
| 				Annotations: map[string]string{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | ||||
| 					op.ItemPathAnnotation: itemPath, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Data: expectedSecretData, | ||||
| @@ -147,6 +153,11 @@ var tests = []testReconcileItem{ | ||||
| 			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, | ||||
| @@ -157,8 +168,10 @@ var tests = []testReconcileItem{ | ||||
| 				Name:      name, | ||||
| 				Namespace: namespace, | ||||
| 				Annotations: map[string]string{ | ||||
| 					op.VersionAnnotation: "456", | ||||
| 					op.VersionAnnotation:  "456", | ||||
| 					op.ItemPathAnnotation: itemPath, | ||||
| 				}, | ||||
| 				Labels: map[string]string{}, | ||||
| 			}, | ||||
| 			Data: expectedSecretData, | ||||
| 		}, | ||||
| @@ -168,8 +181,10 @@ var tests = []testReconcileItem{ | ||||
| 				Name:      name, | ||||
| 				Namespace: namespace, | ||||
| 				Annotations: map[string]string{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 					op.VersionAnnotation:  fmt.Sprint(version), | ||||
| 					op.ItemPathAnnotation: itemPath, | ||||
| 				}, | ||||
| 				Labels: map[string]string{}, | ||||
| 			}, | ||||
| 			Data: expectedSecretData, | ||||
| 		}, | ||||
| @@ -178,6 +193,59 @@ var tests = []testReconcileItem{ | ||||
| 			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, | ||||
| 		}, | ||||
| 		opItem: map[string]string{ | ||||
| 			userKey: username, | ||||
| 			passKey: password, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		testName: "Custom secret type", | ||||
| 		customResource: &onepasswordv1.OnePasswordItem{ | ||||
| @@ -192,6 +260,7 @@ var tests = []testReconcileItem{ | ||||
| 			Spec: onepasswordv1.OnePasswordItemSpec{ | ||||
| 				ItemPath: itemPath, | ||||
| 			}, | ||||
| 			Type: "custom", | ||||
| 		}, | ||||
| 		existingSecret: nil, | ||||
| 		expectedError:  nil, | ||||
| @@ -203,6 +272,7 @@ var tests = []testReconcileItem{ | ||||
| 					op.VersionAnnotation: fmt.Sprint(version), | ||||
| 				}, | ||||
| 			}, | ||||
| 			Type: corev1.SecretType("custom"), | ||||
| 			Data: expectedSecretData, | ||||
| 		}, | ||||
| 		opItem: map[string]string{ | ||||
| @@ -210,6 +280,164 @@ var tests = []testReconcileItem{ | ||||
| 			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) { | ||||
| @@ -241,7 +469,10 @@ func TestReconcileOnePasswordItem(t *testing.T) { | ||||
| 			mocks.GetGetItemFunc = func(uuid string, vaultUUID string) (*onepassword.Item, error) { | ||||
|  | ||||
| 				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.Vault.ID = vaultUUID | ||||
| 				item.ID = uuid | ||||
| @@ -257,8 +488,8 @@ func TestReconcileOnePasswordItem(t *testing.T) { | ||||
| 			// watched resource . | ||||
| 			req := reconcile.Request{ | ||||
| 				NamespacedName: types.NamespacedName{ | ||||
| 					Name:      name, | ||||
| 					Namespace: namespace, | ||||
| 					Name:      testData.customResource.ObjectMeta.Name, | ||||
| 					Namespace: testData.customResource.ObjectMeta.Namespace, | ||||
| 				}, | ||||
| 			} | ||||
| 			_, err := r.Reconcile(req) | ||||
|   | ||||
| @@ -4,12 +4,22 @@ import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | ||||
| 	"reflect" | ||||
|  | ||||
| 	errs "errors" | ||||
|  | ||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | ||||
|  | ||||
| 	"github.com/1Password/onepassword-operator/pkg/utils" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/errors" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	kubeValidate "k8s.io/apimachinery/pkg/util/validation" | ||||
|  | ||||
| 	kubernetesClient "sigs.k8s.io/controller-runtime/pkg/client" | ||||
| 	logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||
| ) | ||||
| @@ -21,24 +31,28 @@ const restartAnnotation = OnepasswordPrefix + "/last-restarted" | ||||
| const ItemPathAnnotation = OnepasswordPrefix + "/item-path" | ||||
| const RestartDeploymentsAnnotation = OnepasswordPrefix + "/auto-restart" | ||||
|  | ||||
| var ErrCannotUpdateSecretType = errs.New("Cannot change secret type. Secret type is immutable") | ||||
|  | ||||
| var log = logf.Log | ||||
|  | ||||
| func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string) error { | ||||
|  | ||||
| func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretName, namespace string, item *onepassword.Item, autoRestart string, labels map[string]string, secretType string, ownerRef *metav1.OwnerReference) error { | ||||
| 	itemVersion := fmt.Sprint(item.Version) | ||||
| 	annotations := map[string]string{ | ||||
| 	secretAnnotations := map[string]string{ | ||||
| 		VersionAnnotation:  itemVersion, | ||||
| 		ItemPathAnnotation: fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID), | ||||
| 	} | ||||
|  | ||||
| 	if autoRestart != "" { | ||||
| 		_, err := utils.StringToBool(autoRestart) | ||||
| 		if err != nil { | ||||
| 			log.Error(err, "Error parsing %v annotation on Secret %v. Must be true or false. Defaulting to false.", RestartDeploymentsAnnotation, secretName) | ||||
| 			return err | ||||
| 		} | ||||
| 		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, ownerRef) | ||||
|  | ||||
| 	currentSecret := &corev1.Secret{} | ||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, currentSecret) | ||||
| @@ -49,9 +63,17 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa | ||||
| 		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)) | ||||
| 		currentSecret.ObjectMeta.Annotations = annotations | ||||
| 		currentSecret.ObjectMeta.Annotations = secretAnnotations | ||||
| 		currentSecret.ObjectMeta.Labels = labels | ||||
| 		currentSecret.Data = secret.Data | ||||
| 		return kubeClient.Update(context.Background(), currentSecret) | ||||
| 	} | ||||
| @@ -60,23 +82,99 @@ func CreateKubernetesSecretFromItem(kubeClient kubernetesClient.Client, secretNa | ||||
| 	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, ownerRef *metav1.OwnerReference) *corev1.Secret { | ||||
| 	var ownerRefs []metav1.OwnerReference | ||||
| 	if ownerRef != nil { | ||||
| 		ownerRefs = []metav1.OwnerReference{*ownerRef} | ||||
| 	} | ||||
|  | ||||
| 	return &corev1.Secret{ | ||||
| 		ObjectMeta: metav1.ObjectMeta{ | ||||
| 			Name:        name, | ||||
| 			Namespace:   namespace, | ||||
| 			Annotations: annotations, | ||||
| 			Name:            formatSecretName(name), | ||||
| 			Namespace:       namespace, | ||||
| 			Annotations:     annotations, | ||||
| 			Labels:          labels, | ||||
| 			OwnerReferences: ownerRefs, | ||||
| 		}, | ||||
| 		Data: BuildKubernetesSecretData(item.Fields), | ||||
| 		Data: BuildKubernetesSecretData(item.Fields, item.Files), | ||||
| 		Type: corev1.SecretType(secretType), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func BuildKubernetesSecretData(fields []*onepassword.ItemField) map[string][]byte { | ||||
| func BuildKubernetesSecretData(fields []*onepassword.ItemField, files []*onepassword.File) map[string][]byte { | ||||
| 	secretData := map[string][]byte{} | ||||
| 	for i := 0; i < len(fields); i++ { | ||||
| 		if fields[i].Value != "" { | ||||
| 			secretData[fields[i].Label] = []byte(fields[i].Value) | ||||
| 			key := formatSecretDataName(fields[i].Label) | ||||
| 			secretData[key] = []byte(fields[i].Value) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// populate unpopulated fields from files | ||||
| 	for _, file := range files { | ||||
| 		content, err := file.Content() | ||||
| 		if err != nil { | ||||
| 			log.Error(err, "Could not load contents of file %s", file.Name) | ||||
| 			continue | ||||
| 		} | ||||
| 		if content != nil { | ||||
| 			key := file.Name | ||||
| 			if secretData[key] == nil { | ||||
| 				secretData[key] = content | ||||
| 			} else { | ||||
| 				log.Info(fmt.Sprintf("File '%s' ignored because of a field with the same name", file.Name)) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return secretData | ||||
| } | ||||
|  | ||||
| // formatSecretName rewrites a value to be a valid Secret name. | ||||
| // | ||||
| // The Secret meta.name and data keys must be valid DNS subdomain names | ||||
| // (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) | ||||
| func formatSecretName(value string) string { | ||||
| 	if errs := kubeValidate.IsDNS1123Subdomain(value); len(errs) == 0 { | ||||
| 		return value | ||||
| 	} | ||||
| 	return createValidSecretName(value) | ||||
| } | ||||
|  | ||||
| // formatSecretDataName rewrites a value to be a valid Secret data key. | ||||
| // | ||||
| // The Secret data keys must consist of alphanumeric numbers, `-`, `_` or `.` | ||||
| // (https://kubernetes.io/docs/concepts/configuration/secret/#overview-of-secrets) | ||||
| func formatSecretDataName(value string) string { | ||||
| 	if errs := kubeValidate.IsConfigMapKey(value); len(errs) == 0 { | ||||
| 		return value | ||||
| 	} | ||||
| 	return createValidSecretDataName(value) | ||||
| } | ||||
|  | ||||
| var invalidDNS1123Chars = regexp.MustCompile("[^a-z0-9-.]+") | ||||
|  | ||||
| func createValidSecretName(value string) string { | ||||
| 	result := strings.ToLower(value) | ||||
| 	result = invalidDNS1123Chars.ReplaceAllString(result, "-") | ||||
|  | ||||
| 	if len(result) > kubeValidate.DNS1123SubdomainMaxLength { | ||||
| 		result = result[0:kubeValidate.DNS1123SubdomainMaxLength] | ||||
| 	} | ||||
|  | ||||
| 	// first and last character MUST be alphanumeric | ||||
| 	return strings.Trim(result, "-.") | ||||
| } | ||||
|  | ||||
| var invalidDataChars = regexp.MustCompile("[^a-zA-Z0-9-._]+") | ||||
| var invalidStartEndChars = regexp.MustCompile("(^[^a-zA-Z0-9-._]+|[^a-zA-Z0-9-._]+$)") | ||||
|  | ||||
| func createValidSecretDataName(value string) string { | ||||
| 	result := invalidStartEndChars.ReplaceAllString(value, "") | ||||
| 	result = invalidDataChars.ReplaceAllString(result, "-") | ||||
|  | ||||
| 	if len(result) > kubeValidate.DNS1123SubdomainMaxLength { | ||||
| 		result = result[0:kubeValidate.DNS1123SubdomainMaxLength] | ||||
| 	} | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,9 @@ import ( | ||||
|  | ||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | ||||
| 	corev1 "k8s.io/api/core/v1" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/types" | ||||
| 	kubeValidate "k8s.io/apimachinery/pkg/util/validation" | ||||
| 	"k8s.io/client-go/kubernetes" | ||||
| 	"sigs.k8s.io/controller-runtime/pkg/client/fake" | ||||
| ) | ||||
| @@ -30,7 +32,10 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||
|  | ||||
| 	kubeClient := fake.NewFakeClient() | ||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation) | ||||
| 	secretLabels := map[string]string{} | ||||
| 	secretType := "" | ||||
|  | ||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Unexpected error: %v", err) | ||||
| 	} | ||||
| @@ -44,6 +49,51 @@ func TestCreateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||
| 	compareAnnotationsToItem(createdSecret.Annotations, item, t) | ||||
| } | ||||
|  | ||||
| func TestKubernetesSecretFromOnePasswordItemOwnerReferences(t *testing.T) { | ||||
| 	secretName := "test-secret-name" | ||||
| 	namespace := "test" | ||||
|  | ||||
| 	item := onepassword.Item{} | ||||
| 	item.Fields = generateFields(5) | ||||
| 	item.Version = 123 | ||||
| 	item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | ||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||
|  | ||||
| 	kubeClient := fake.NewFakeClient() | ||||
| 	secretLabels := map[string]string{} | ||||
| 	secretType := "" | ||||
|  | ||||
| 	ownerRef := &metav1.OwnerReference{ | ||||
| 		Kind:       "Deployment", | ||||
| 		APIVersion: "apps/v1", | ||||
| 		Name:       "test-deployment", | ||||
| 		UID:        types.UID("test-uid"), | ||||
| 	} | ||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, ownerRef) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Unexpected error: %v", err) | ||||
| 	} | ||||
| 	createdSecret := &corev1.Secret{} | ||||
| 	err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) | ||||
|  | ||||
| 	// Check owner references. | ||||
| 	gotOwnerRefs := createdSecret.ObjectMeta.OwnerReferences | ||||
| 	if len(gotOwnerRefs) != 1 { | ||||
| 		t.Errorf("Expected owner references length: 1 but got: %d", len(gotOwnerRefs)) | ||||
| 	} | ||||
|  | ||||
| 	expOwnerRef := metav1.OwnerReference{ | ||||
| 		Kind:       "Deployment", | ||||
| 		APIVersion: "apps/v1", | ||||
| 		Name:       "test-deployment", | ||||
| 		UID:        types.UID("test-uid"), | ||||
| 	} | ||||
| 	gotOwnerRef := gotOwnerRefs[0] | ||||
| 	if gotOwnerRef != expOwnerRef { | ||||
| 		t.Errorf("Expected owner reference value: %v but got: %v", expOwnerRef, gotOwnerRef) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||
| 	secretName := "test-secret-update" | ||||
| 	namespace := "test" | ||||
| @@ -55,7 +105,11 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||
|  | ||||
| 	kubeClient := fake.NewFakeClient() | ||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation) | ||||
| 	secretLabels := map[string]string{} | ||||
| 	secretType := "" | ||||
|  | ||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Unexpected error: %v", err) | ||||
| 	} | ||||
| @@ -66,7 +120,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||
| 	newItem.Version = 456 | ||||
| 	newItem.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | ||||
| 	newItem.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||
| 	err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation) | ||||
| 	err = CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &newItem, restartDeploymentAnnotation, secretLabels, secretType, nil) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Unexpected error: %v", err) | ||||
| 	} | ||||
| @@ -82,7 +136,7 @@ func TestUpdateKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||
| func TestBuildKubernetesSecretData(t *testing.T) { | ||||
| 	fields := generateFields(5) | ||||
|  | ||||
| 	secretData := BuildKubernetesSecretData(fields) | ||||
| 	secretData := BuildKubernetesSecretData(fields, nil) | ||||
| 	if len(secretData) != len(fields) { | ||||
| 		t.Errorf("Unexpected number of secret fields returned. Expected 3, got %v", len(secretData)) | ||||
| 	} | ||||
| @@ -99,9 +153,11 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.T) { | ||||
| 	} | ||||
| 	item := onepassword.Item{} | ||||
| 	item.Fields = generateFields(5) | ||||
| 	labels := map[string]string{} | ||||
| 	secretType := "" | ||||
|  | ||||
| 	kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, item) | ||||
| 	if kubeSecret.Name != name { | ||||
| 	kubeSecret := BuildKubernetesSecretFromOnePasswordItem(name, namespace, annotations, labels, secretType, item, nil) | ||||
| 	if kubeSecret.Name != strings.ToLower(name) { | ||||
| 		t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) | ||||
| 	} | ||||
| 	if kubeSecret.Namespace != namespace { | ||||
| @@ -113,6 +169,76 @@ func TestBuildKubernetesSecretFromOnePasswordItem(t *testing.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, nil) | ||||
|  | ||||
| 	// Assert Secret's meta.name was fixed | ||||
| 	if kubeSecret.Name != expectedName { | ||||
| 		t.Errorf("Expected name value: %v but got: %v", name, kubeSecret.Name) | ||||
| 	} | ||||
| 	if kubeSecret.Namespace != namespace { | ||||
| 		t.Errorf("Expected namespace value: %v but got: %v", namespace, kubeSecret.Namespace) | ||||
| 	} | ||||
|  | ||||
| 	// assert labels were fixed for each data key | ||||
| 	for key := range kubeSecret.Data { | ||||
| 		if !validLabel(key) { | ||||
| 			t.Errorf("Expected valid kubernetes label, got %s", key) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCreateKubernetesTLSSecretFromOnePasswordItem(t *testing.T) { | ||||
| 	secretName := "tls-test-secret-name" | ||||
| 	namespace := "test" | ||||
|  | ||||
| 	item := onepassword.Item{} | ||||
| 	item.Fields = generateFields(5) | ||||
| 	item.Version = 123 | ||||
| 	item.Vault.ID = "hfnjvi6aymbsnfc2xeeoheizda" | ||||
| 	item.ID = "h46bb3jddvay7nxopfhvlwg35q" | ||||
|  | ||||
| 	kubeClient := fake.NewFakeClient() | ||||
| 	secretLabels := map[string]string{} | ||||
| 	secretType := "kubernetes.io/tls" | ||||
|  | ||||
| 	err := CreateKubernetesSecretFromItem(kubeClient, secretName, namespace, &item, restartDeploymentAnnotation, secretLabels, secretType, nil) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Unexpected error: %v", err) | ||||
| 	} | ||||
| 	createdSecret := &corev1.Secret{} | ||||
| 	err = kubeClient.Get(context.Background(), types.NamespacedName{Name: secretName, Namespace: namespace}, createdSecret) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Secret was not created: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	if createdSecret.Type != corev1.SecretTypeTLS { | ||||
| 		t.Errorf("Expected secretType to be of tyype corev1.SecretTypeTLS, got %s", string(createdSecret.Type)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func compareAnnotationsToItem(annotations map[string]string, item onepassword.Item, t *testing.T) { | ||||
| 	actualVaultId, actualItemId, err := ParseVaultIdAndItemIdFromPath(annotations[ItemPathAnnotation]) | ||||
| 	if err != nil { | ||||
| @@ -164,3 +290,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) | ||||
| } | ||||
|  | ||||
| func validLabel(v string) bool { | ||||
| 	if err := kubeValidate.IsConfigMapKey(v); len(err) > 0 { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import ( | ||||
| type TestClient struct { | ||||
| 	GetVaultsFunc        func() ([]onepassword.Vault, error) | ||||
| 	GetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) | ||||
| 	GetVaultFunc         func(uuid string) (*onepassword.Vault, error) | ||||
| 	GetItemFunc          func(uuid string, vaultUUID string) (*onepassword.Item, error) | ||||
| 	GetItemsFunc         func(vaultUUID string) ([]onepassword.Item, error) | ||||
| 	GetItemsByTitleFunc  func(title string, vaultUUID string) ([]onepassword.Item, error) | ||||
| @@ -14,11 +15,14 @@ type TestClient struct { | ||||
| 	CreateItemFunc       func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||
| 	UpdateItemFunc       func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||
| 	DeleteItemFunc       func(item *onepassword.Item, vaultUUID string) error | ||||
| 	GetFileFunc          func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) | ||||
| 	GetFileContentFunc   func(file *onepassword.File) ([]byte, error) | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	GetGetVaultsFunc       func() ([]onepassword.Vault, error) | ||||
| 	DoGetVaultsByTitleFunc func(title string) ([]onepassword.Vault, error) | ||||
| 	DoGetVaultFunc         func(uuid string) (*onepassword.Vault, error) | ||||
| 	GetGetItemFunc         func(uuid string, vaultUUID string) (*onepassword.Item, error) | ||||
| 	DoGetItemsByTitleFunc  func(title string, vaultUUID string) ([]onepassword.Item, error) | ||||
| 	DoGetItemByTitleFunc   func(title string, vaultUUID string) (*onepassword.Item, error) | ||||
| @@ -26,6 +30,8 @@ var ( | ||||
| 	DoDeleteItemFunc       func(item *onepassword.Item, vaultUUID string) error | ||||
| 	DoGetItemsFunc         func(vaultUUID string) ([]onepassword.Item, error) | ||||
| 	DoUpdateItemFunc       func(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||
| 	DoGetFileFunc          func(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) | ||||
| 	DoGetFileContentFunc   func(file *onepassword.File) ([]byte, error) | ||||
| ) | ||||
|  | ||||
| // Do is the mock client's `Do` func | ||||
| @@ -37,6 +43,10 @@ func (m *TestClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) | ||||
| 	return DoGetVaultsByTitleFunc(title) | ||||
| } | ||||
|  | ||||
| func (m *TestClient) GetVault(uuid string) (*onepassword.Vault, error) { | ||||
| 	return DoGetVaultFunc(uuid) | ||||
| } | ||||
|  | ||||
| func (m *TestClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) { | ||||
| 	return GetGetItemFunc(uuid, vaultUUID) | ||||
| } | ||||
| @@ -64,3 +74,11 @@ func (m *TestClient) DeleteItem(item *onepassword.Item, vaultUUID string) error | ||||
| func (m *TestClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) { | ||||
| 	return DoUpdateItemFunc(item, vaultUUID) | ||||
| } | ||||
|  | ||||
| func (m *TestClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) { | ||||
| 	return DoGetFileFunc(uuid, itemUUID, vaultUUID) | ||||
| } | ||||
|  | ||||
| func (m *TestClient) GetFileContent(file *onepassword.File) ([]byte, error) { | ||||
| 	return DoGetFileContentFunc(file) | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package onepassword | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"os" | ||||
|  | ||||
| 	appsv1 "k8s.io/api/apps/v1" | ||||
| @@ -17,13 +18,13 @@ 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) | ||||
| func SetupConnect(kubeClient client.Client, deploymentNamespace string) error { | ||||
| 	err := setupService(kubeClient, servicePath, deploymentNamespace) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = setupDeployment(kubeClient, deploymentPath) | ||||
| 	err = setupDeployment(kubeClient, deploymentPath, deploymentNamespace) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -31,22 +32,22 @@ func SetupConnect(kubeClient client.Client) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func setupDeployment(kubeClient client.Client, deploymentPath string) error { | ||||
| func setupDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { | ||||
| 	existingDeployment := &appsv1.Deployment{} | ||||
|  | ||||
| 	// check if deployment has already been created | ||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingDeployment) | ||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingDeployment) | ||||
| 	if err != nil { | ||||
| 		if errors.IsNotFound(err) { | ||||
| 			logConnectSetup.Info("No existing Connect deployment found. Creating Deployment") | ||||
| 			return createDeployment(kubeClient, deploymentPath) | ||||
| 			return createDeployment(kubeClient, deploymentPath, deploymentNamespace) | ||||
| 		} | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func createDeployment(kubeClient client.Client, deploymentPath string) error { | ||||
| 	deployment, err := getDeploymentToCreate(deploymentPath) | ||||
| func createDeployment(kubeClient client.Client, deploymentPath string, deploymentNamespace string) error { | ||||
| 	deployment, err := getDeploymentToCreate(deploymentPath, deploymentNamespace) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -59,12 +60,16 @@ func createDeployment(kubeClient client.Client, deploymentPath string) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) { | ||||
| func getDeploymentToCreate(deploymentPath string, deploymentNamespace string) (*appsv1.Deployment, error) { | ||||
| 	f, err := os.Open(deploymentPath) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	deployment := &appsv1.Deployment{} | ||||
| 	deployment := &appsv1.Deployment{ | ||||
| 		ObjectMeta: v1.ObjectMeta{ | ||||
| 			Namespace: deploymentNamespace, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(deployment) | ||||
| 	if err != nil { | ||||
| @@ -73,26 +78,30 @@ func getDeploymentToCreate(deploymentPath string) (*appsv1.Deployment, error) { | ||||
| 	return deployment, nil | ||||
| } | ||||
|  | ||||
| func setupService(kubeClient client.Client, servicePath string) error { | ||||
| func setupService(kubeClient client.Client, servicePath string, deploymentNamespace string) error { | ||||
| 	existingService := &corev1.Service{} | ||||
|  | ||||
| 	//check if service has already been created | ||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: "default"}, existingService) | ||||
| 	err := kubeClient.Get(context.Background(), types.NamespacedName{Name: "onepassword-connect", Namespace: deploymentNamespace}, existingService) | ||||
| 	if err != nil { | ||||
| 		if errors.IsNotFound(err) { | ||||
| 			logConnectSetup.Info("No existing Connect service found. Creating Service") | ||||
| 			return createService(kubeClient, servicePath) | ||||
| 			return createService(kubeClient, servicePath, deploymentNamespace) | ||||
| 		} | ||||
| 	} | ||||
| 	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) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	service := &corev1.Service{} | ||||
| 	service := &corev1.Service{ | ||||
| 		ObjectMeta: v1.ObjectMeta{ | ||||
| 			Namespace: deploymentNamespace, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	err = yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(service) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -25,7 +25,7 @@ func TestServiceSetup(t *testing.T) { | ||||
| 	// Create a fake client to mock API calls. | ||||
| 	client := fake.NewFakeClientWithScheme(s, objs...) | ||||
|  | ||||
| 	err := setupService(client, "../../deploy/connect/service.yaml") | ||||
| 	err := setupService(client, "../../deploy/connect/service.yaml", defaultNamespacedName.Namespace) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		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. | ||||
| 	client := fake.NewFakeClientWithScheme(s, objs...) | ||||
|  | ||||
| 	err := setupDeployment(client, "../../deploy/connect/deployment.yaml") | ||||
| 	err := setupDeployment(client, "../../deploy/connect/deployment.yaml", defaultNamespacedName.Namespace) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Error Setting Up Connect: %v", err) | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| 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 { | ||||
| 	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 | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -4,9 +4,10 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	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{ | ||||
| 		"onepassword-database-secret": &corev1.Secret{}, | ||||
| 		"onepassword-api-key":         &corev1.Secret{}, | ||||
| @@ -18,7 +19,26 @@ func TestAreContainersUsingSecrets(t *testing.T) { | ||||
| 		"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) { | ||||
| 		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) { | ||||
| 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||
| 		"onepassword-database-secret": &corev1.Secret{}, | ||||
| 		"onepassword-api-key":         &corev1.Secret{}, | ||||
| 		"onepassword-database-secret": {}, | ||||
| 		"onepassword-api-key":         {}, | ||||
| 	} | ||||
|  | ||||
| 	containerSecretNames := []string{ | ||||
| 		"some_other_key", | ||||
| 	} | ||||
|  | ||||
| 	containers := generateContainers(containerSecretNames) | ||||
| 	containers := generateContainersWithSecretRefsFromEnv(containerSecretNames) | ||||
|  | ||||
| 	if AreContainersUsingSecrets(containers, secretNamesToSearch) { | ||||
| 		t.Errorf("Expected that containers were not using secrets but they were detected.") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAppendUpdatedContainerSecretsParsesEnvFromEnv(t *testing.T) { | ||||
| 	secretNamesToSearch := map[string]*corev1.Secret{ | ||||
| 		"onepassword-database-secret": {}, | ||||
| 		"onepassword-api-key":         {ObjectMeta: metav1.ObjectMeta{Name: "onepassword-api-key"}}, | ||||
| 	} | ||||
|  | ||||
| 	containerSecretNames := []string{ | ||||
| 		"onepassword-api-key", | ||||
| 	} | ||||
|  | ||||
| 	containers := generateContainersWithSecretRefsFromEnvFrom(containerSecretNames) | ||||
|  | ||||
| 	updatedDeploymentSecrets := map[string]*corev1.Secret{} | ||||
| 	updatedDeploymentSecrets = AppendUpdatedContainerSecrets(containers, secretNamesToSearch, updatedDeploymentSecrets) | ||||
|  | ||||
| 	secretKeyName := "onepassword-api-key" | ||||
|  | ||||
| 	if updatedDeploymentSecrets[secretKeyName] != secretNamesToSearch[secretKeyName] { | ||||
| 		t.Errorf("Expected that updated Secret from envfrom is found.") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -39,7 +39,7 @@ func TestIsDeploymentUsingSecretsUsingContainers(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	deployment := &appsv1.Deployment{} | ||||
| 	deployment.Spec.Template.Spec.Containers = generateContainers(containerSecretNames) | ||||
| 	deployment.Spec.Template.Spec.Containers = generateContainersWithSecretRefsFromEnv(containerSecretNames) | ||||
| 	if !IsDeploymentUsingSecrets(deployment, secretNamesToSearch) { | ||||
| 		t.Errorf("Expected that deployment was using secrets but they were not detected.") | ||||
| 	} | ||||
|   | ||||
| @@ -30,6 +30,14 @@ func GetOnePasswordItemByPath(opConnectClient connect.Client, path string) (*one | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for _, file := range item.Files { | ||||
| 		_, err := opConnectClient.GetFileContent(file) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return item, nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -17,8 +17,7 @@ func generateVolumes(names []string) []corev1.Volume { | ||||
| 	} | ||||
| 	return volumes | ||||
| } | ||||
|  | ||||
| func generateContainers(names []string) []corev1.Container { | ||||
| func generateContainersWithSecretRefsFromEnv(names []string) []corev1.Container { | ||||
| 	containers := []corev1.Container{} | ||||
| 	for i := 0; i < len(names); i++ { | ||||
| 		container := corev1.Container{ | ||||
| @@ -40,3 +39,16 @@ func generateContainers(names []string) []corev1.Container { | ||||
| 	} | ||||
| 	return containers | ||||
| } | ||||
|  | ||||
| func generateContainersWithSecretRefsFromEnvFrom(names []string) []corev1.Container { | ||||
| 	containers := []corev1.Container{} | ||||
| 	for i := 0; i < len(names); i++ { | ||||
| 		container := corev1.Container{ | ||||
| 			EnvFrom: []corev1.EnvFromSource{ | ||||
| 				{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: names[i]}}}, | ||||
| 			}, | ||||
| 		} | ||||
| 		containers = append(containers, container) | ||||
| 	} | ||||
| 	return containers | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,8 @@ import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	v1 "github.com/1Password/onepassword-operator/pkg/apis/onepassword/v1" | ||||
|  | ||||
| 	kubeSecrets "github.com/1Password/onepassword-operator/pkg/kubernetessecrets" | ||||
| 	"github.com/1Password/onepassword-operator/pkg/utils" | ||||
|  | ||||
| @@ -116,23 +118,37 @@ func (h *SecretUpdateHandler) updateKubernetesSecrets() (map[string]map[string]* | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		item, err := GetOnePasswordItemByPath(h.opConnectClient, secret.Annotations[ItemPathAnnotation]) | ||||
| 		OnePasswordItemPath := h.getPathFromOnePasswordItem(secret) | ||||
|  | ||||
| 		item, err := GetOnePasswordItemByPath(h.opConnectClient, OnePasswordItemPath) | ||||
| 		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) | ||||
| 		if currentVersion != itemVersion { | ||||
| 		itemPathString := fmt.Sprintf("vaults/%v/items/%v", item.Vault.ID, item.ID) | ||||
|  | ||||
| 		if currentVersion != itemVersion || secret.Annotations[ItemPathAnnotation] != itemPathString { | ||||
| 			if isItemLockedForForcedRestarts(item) { | ||||
| 				log.Info(fmt.Sprintf("Secret '%v' has been updated in 1Password but is set to be ignored. Updates to an ignored secret will not trigger an update to a kubernetes secret or a rolling restart.", secret.GetName())) | ||||
| 				secret.Annotations[VersionAnnotation] = itemVersion | ||||
| 				h.client.Update(context.Background(), &secret) | ||||
| 				secret.Annotations[ItemPathAnnotation] = itemPathString | ||||
| 				if err := h.client.Update(context.Background(), &secret); err != nil { | ||||
| 					log.Error(err, "failed to update secret %s annotations to version %d: %s", secret.Name, itemVersion, err) | ||||
| 					continue | ||||
| 				} | ||||
| 				continue | ||||
| 			} | ||||
| 			log.Info(fmt.Sprintf("Updating kubernetes secret '%v'", secret.GetName())) | ||||
| 			secret.Annotations[VersionAnnotation] = itemVersion | ||||
| 			updatedSecret := kubeSecrets.BuildKubernetesSecretFromOnePasswordItem(secret.Name, secret.Namespace, secret.Annotations, *item) | ||||
| 			h.client.Update(context.Background(), updatedSecret) | ||||
| 			secret.Annotations[ItemPathAnnotation] = itemPathString | ||||
| 			secret.Data = kubeSecrets.BuildKubernetesSecretData(item.Fields, item.Files) | ||||
| 			log.Info(fmt.Sprintf("New secret path: %v and version: %v", secret.Annotations[ItemPathAnnotation], secret.Annotations[VersionAnnotation])) | ||||
| 			if err := h.client.Update(context.Background(), &secret); err != nil { | ||||
| 				log.Error(err, "failed to update secret %s to version %d: %s", secret.Name, itemVersion, err) | ||||
| 				continue | ||||
| 			} | ||||
| 			if updatedSecrets[secret.Namespace] == nil { | ||||
| 				updatedSecrets[secret.Namespace] = make(map[string]*corev1.Secret) | ||||
| 			} | ||||
| @@ -176,6 +192,22 @@ func (h *SecretUpdateHandler) getIsSetForAutoRestartByNamespaceMap() (map[string | ||||
| 	return namespacesMap, nil | ||||
| } | ||||
|  | ||||
| func (h *SecretUpdateHandler) getPathFromOnePasswordItem(secret corev1.Secret) string { | ||||
| 	onePasswordItem := &v1.OnePasswordItem{} | ||||
|  | ||||
| 	// Search for our original OnePasswordItem if it exists | ||||
| 	err := h.client.Get(context.TODO(), client.ObjectKey{ | ||||
| 		Namespace: secret.Namespace, | ||||
| 		Name:      secret.Name}, onePasswordItem) | ||||
|  | ||||
| 	if err == nil { | ||||
| 		return onePasswordItem.Spec.ItemPath | ||||
| 	} | ||||
|  | ||||
| 	// If we can't find the OnePassword Item we'll just return the annotation from the secret item. | ||||
| 	return secret.Annotations[ItemPathAnnotation] | ||||
| } | ||||
|  | ||||
| func isSecretSetForAutoRestart(secret *corev1.Secret, deployment *appsv1.Deployment, setForAutoRestartByNamespace map[string]bool) bool { | ||||
| 	restartDeployment := secret.Annotations[RestartDeploymentsAnnotation] | ||||
| 	//If annotation for auto restarts for deployment is not set. Check for the annotation on its namepsace | ||||
|   | ||||
							
								
								
									
										21
									
								
								vendor/github.com/1Password/connect-sdk-go/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								vendor/github.com/1Password/connect-sdk-go/LICENSE
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2021 1Password | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										219
									
								
								vendor/github.com/1Password/connect-sdk-go/connect/client.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										219
									
								
								vendor/github.com/1Password/connect-sdk-go/connect/client.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ package connect | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| @@ -10,11 +11,12 @@ import ( | ||||
| 	"net/url" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | ||||
| 	opentracing "github.com/opentracing/opentracing-go" | ||||
| 	"github.com/opentracing/opentracing-go/ext" | ||||
| 	jaegerClientConfig "github.com/uber/jaeger-client-go/config" | ||||
| 	"github.com/uber/jaeger-client-go/zipkin" | ||||
|  | ||||
| 	"github.com/1Password/connect-sdk-go/onepassword" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -24,6 +26,7 @@ const ( | ||||
| // Client Represents an available 1Password Connect API to connect to | ||||
| type Client interface { | ||||
| 	GetVaults() ([]onepassword.Vault, error) | ||||
| 	GetVault(uuid string) (*onepassword.Vault, error) | ||||
| 	GetVaultsByTitle(uuid string) ([]onepassword.Vault, error) | ||||
| 	GetItem(uuid string, vaultUUID string) (*onepassword.Item, error) | ||||
| 	GetItems(vaultUUID string) ([]onepassword.Item, error) | ||||
| @@ -32,6 +35,8 @@ type Client interface { | ||||
| 	CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||
| 	UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) | ||||
| 	DeleteItem(item *onepassword.Item, vaultUUID string) error | ||||
| 	GetFile(fileUUID string, itemUUID string, vaultUUID string) (*onepassword.File, error) | ||||
| 	GetFileContent(file *onepassword.File) ([]byte, error) | ||||
| } | ||||
|  | ||||
| type httpClient interface { | ||||
| @@ -112,23 +117,41 @@ func (rs *restClient) GetVaults() ([]onepassword.Vault, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("Unable to retrieve vaults. Receieved %q for %q", response.Status, vaultURL) | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(response.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	vaults := []onepassword.Vault{} | ||||
| 	if err := json.Unmarshal(body, &vaults); err != nil { | ||||
| 	var vaults []onepassword.Vault | ||||
| 	if err := parseResponse(response, http.StatusOK, &vaults); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return vaults, nil | ||||
| } | ||||
|  | ||||
| // GetVaults Get a list of all available vaults | ||||
| func (rs *restClient) GetVault(uuid string) (*onepassword.Vault, error) { | ||||
| 	if uuid == "" { | ||||
| 		return nil, errors.New("no uuid provided") | ||||
| 	} | ||||
|  | ||||
| 	span := rs.tracer.StartSpan("GetVault") | ||||
| 	defer span.Finish() | ||||
|  | ||||
| 	vaultURL := fmt.Sprintf("/v1/vaults/%s", uuid) | ||||
| 	request, err := rs.buildRequest(http.MethodGet, vaultURL, http.NoBody, span) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	response, err := rs.client.Do(request) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var vault onepassword.Vault | ||||
| 	if err := parseResponse(response, http.StatusOK, &vault); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &vault, nil | ||||
| } | ||||
|  | ||||
| func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error) { | ||||
| 	span := rs.tracer.StartSpan("GetVaultsByTitle") | ||||
| 	defer span.Finish() | ||||
| @@ -145,17 +168,8 @@ func (rs *restClient) GetVaultsByTitle(title string) ([]onepassword.Vault, error | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("Unable to retrieve vaults. Receieved %q for %q", response.Status, itemURL) | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(response.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	vaults := []onepassword.Vault{} | ||||
| 	if err := json.Unmarshal(body, &vaults); err != nil { | ||||
| 	var vaults []onepassword.Vault | ||||
| 	if err := parseResponse(response, http.StatusOK, &vaults); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -177,18 +191,8 @@ func (rs *restClient) GetItem(uuid string, vaultUUID string) (*onepassword.Item, | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL) | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(response.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	item := onepassword.Item{} | ||||
| 	if err := json.Unmarshal(body, &item); err != nil { | ||||
| 	var item onepassword.Item | ||||
| 	if err := parseResponse(response, http.StatusOK, &item); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -226,17 +230,8 @@ func (rs *restClient) GetItemsByTitle(title string, vaultUUID string) ([]onepass | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL) | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(response.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	items := []onepassword.Item{} | ||||
| 	if err := json.Unmarshal(body, &items); err != nil { | ||||
| 	var items []onepassword.Item | ||||
| 	if err := parseResponse(response, http.StatusOK, &items); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -258,17 +253,8 @@ func (rs *restClient) GetItems(vaultUUID string) ([]onepassword.Item, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("Unable to retrieve items. Receieved %q for %q", response.Status, itemURL) | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(response.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	items := []onepassword.Item{} | ||||
| 	if err := json.Unmarshal(body, &items); err != nil { | ||||
| 	var items []onepassword.Item | ||||
| 	if err := parseResponse(response, http.StatusOK, &items); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -296,17 +282,8 @@ func (rs *restClient) CreateItem(item *onepassword.Item, vaultUUID string) (*one | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("Unable to create item. Receieved %q for %q", response.Status, itemURL) | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(response.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	newItem := onepassword.Item{} | ||||
| 	if err := json.Unmarshal(body, &newItem); err != nil { | ||||
| 	var newItem onepassword.Item | ||||
| 	if err := parseResponse(response, http.StatusOK, &newItem); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -334,17 +311,8 @@ func (rs *restClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*one | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusOK { | ||||
| 		return nil, fmt.Errorf("Unable to update item. Receieved %q for %q", response.Status, itemURL) | ||||
| 	} | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(response.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	newItem := onepassword.Item{} | ||||
| 	if err := json.Unmarshal(body, &newItem); err != nil { | ||||
| 	var newItem onepassword.Item | ||||
| 	if err := parseResponse(response, http.StatusOK, &newItem); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -367,13 +335,73 @@ func (rs *restClient) DeleteItem(item *onepassword.Item, vaultUUID string) error | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if response.StatusCode != http.StatusNoContent { | ||||
| 		return fmt.Errorf("Unable to retrieve item. Receieved %q for %q", response.Status, itemURL) | ||||
| 	if err := parseResponse(response, http.StatusNoContent, nil); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // GetFile Get a specific File in a specified item. | ||||
| // This does not include the file contents. Call GetFileContent() to load the file's content. | ||||
| func (rs *restClient) GetFile(uuid string, itemUUID string, vaultUUID string) (*onepassword.File, error) { | ||||
| 	span := rs.tracer.StartSpan("GetFile") | ||||
| 	defer span.Finish() | ||||
|  | ||||
| 	itemURL := fmt.Sprintf("/v1/vaults/%s/items/%s/files/%s", vaultUUID, itemUUID, uuid) | ||||
| 	request, err := rs.buildRequest(http.MethodGet, itemURL, http.NoBody, span) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	response, err := rs.client.Do(request) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var file onepassword.File | ||||
| 	if err := parseResponse(response, http.StatusOK, &file); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &file, nil | ||||
| } | ||||
|  | ||||
| // GetFileContent retrieves the file's content. | ||||
| // If the file's content have previously been fetched, those contents are returned without making another request. | ||||
| func (rs *restClient) GetFileContent(file *onepassword.File) ([]byte, error) { | ||||
| 	if content, err := file.Content(); err == nil { | ||||
| 		return content, nil | ||||
| 	} | ||||
|  | ||||
| 	span := rs.tracer.StartSpan("GetFileContent") | ||||
| 	defer span.Finish() | ||||
|  | ||||
| 	request, err := rs.buildRequest(http.MethodGet, file.ContentPath, http.NoBody, span) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	response, err := rs.client.Do(request) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := expectMinimumConnectVersion(response, version{1, 3, 0}); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	content, err := readResponseBody(response, http.StatusOK) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	file.SetContent(content) | ||||
| 	return content, nil | ||||
| } | ||||
|  | ||||
| func (rs *restClient) buildRequest(method string, path string, body io.Reader, span opentracing.Span) (*http.Request, error) { | ||||
| 	url := fmt.Sprintf("%s%s", rs.URL, path) | ||||
|  | ||||
| @@ -394,3 +422,32 @@ func (rs *restClient) buildRequest(method string, path string, body io.Reader, s | ||||
|  | ||||
| 	return request, nil | ||||
| } | ||||
|  | ||||
| func parseResponse(resp *http.Response, expectedStatusCode int, result interface{}) error { | ||||
| 	body, err := readResponseBody(resp, expectedStatusCode) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if result != nil { | ||||
| 		if err := json.Unmarshal(body, result); err != nil { | ||||
| 			return fmt.Errorf("decoding response: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func readResponseBody(resp *http.Response, expectedStatusCode int) ([]byte, error) { | ||||
| 	defer resp.Body.Close() | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if resp.StatusCode != expectedStatusCode { | ||||
| 		var errResp *onepassword.Error | ||||
| 		if err := json.Unmarshal(body, &errResp); err != nil { | ||||
| 			return nil, fmt.Errorf("decoding error response: %s", err) | ||||
| 		} | ||||
| 		return nil, errResp | ||||
| 	} | ||||
| 	return body, nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										101
									
								
								vendor/github.com/1Password/connect-sdk-go/connect/version.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										101
									
								
								vendor/github.com/1Password/connect-sdk-go/connect/version.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -1,5 +1,104 @@ | ||||
| package connect | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| // SDKVersion is the latest Semantic Version of the library | ||||
| // Do not rename this variable without changing the regex in the Makefile | ||||
| const SDKVersion = "1.0.1" | ||||
| const SDKVersion = "1.2.0" | ||||
|  | ||||
| const VersionHeaderKey = "1Password-Connect-Version" | ||||
|  | ||||
| // expectMinimumConnectVersion returns an error if the provided minimum version for Connect is lower than the version | ||||
| // reported in the response from Connect. | ||||
| func expectMinimumConnectVersion(resp *http.Response, minimumVersion version) error { | ||||
| 	serverVersion, err := getServerVersion(resp) | ||||
| 	if err != nil { | ||||
| 		// Return gracefully if server version cannot be determined reliably | ||||
| 		return nil | ||||
| 	} | ||||
| 	if !serverVersion.IsGreaterOrEqualThan(minimumVersion) { | ||||
| 		return fmt.Errorf("need at least version %s of Connect for this function, detected version %s. Please update your Connect server", minimumVersion, serverVersion) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func getServerVersion(resp *http.Response) (serverVersion, error) { | ||||
| 	versionHeader := resp.Header.Get(VersionHeaderKey) | ||||
| 	if versionHeader == "" { | ||||
| 		// The last version without the version header was v1.2.0 | ||||
| 		return serverVersion{ | ||||
| 			version:   version{1, 2, 0}, | ||||
| 			orEarlier: true, | ||||
| 		}, nil | ||||
| 	} | ||||
| 	return parseServerVersion(versionHeader) | ||||
| } | ||||
|  | ||||
| type version struct { | ||||
| 	major int | ||||
| 	minor int | ||||
| 	patch int | ||||
| } | ||||
|  | ||||
| // serverVersion describes the version reported by the server. | ||||
| type serverVersion struct { | ||||
| 	version | ||||
| 	// orEarlier is true if the version is derived from the lack of a version header from the server. | ||||
| 	orEarlier bool | ||||
| } | ||||
|  | ||||
| func (v version) String() string { | ||||
| 	return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch) | ||||
| } | ||||
|  | ||||
| func (v serverVersion) String() string { | ||||
| 	if v.orEarlier { | ||||
| 		return v.version.String() + " (or earlier)" | ||||
| 	} | ||||
| 	return v.version.String() | ||||
| } | ||||
|  | ||||
| // IsGreaterOrEqualThan returns true if the lefthand-side version is equal to or or a higher version than the provided | ||||
| // minimum according to the semantic versioning rules. | ||||
| func (v version) IsGreaterOrEqualThan(min version) bool { | ||||
| 	if v.major != min.major { | ||||
| 		// Different major version | ||||
| 		return v.major > min.major | ||||
| 	} | ||||
|  | ||||
| 	if v.minor != min.minor { | ||||
| 		// Same major, but different minor version | ||||
| 		return v.minor > min.minor | ||||
| 	} | ||||
|  | ||||
| 	// Same major and minor version | ||||
| 	return v.patch >= min.patch | ||||
| } | ||||
|  | ||||
| func parseServerVersion(v string) (serverVersion, error) { | ||||
| 	spl := strings.Split(v, ".") | ||||
| 	if len(spl) != 3 { | ||||
| 		return serverVersion{}, errors.New("wrong length") | ||||
| 	} | ||||
| 	var res [3]int | ||||
| 	for i := range res { | ||||
| 		tmp, err := strconv.Atoi(spl[i]) | ||||
| 		if err != nil { | ||||
| 			return serverVersion{}, err | ||||
| 		} | ||||
| 		res[i] = tmp | ||||
| 	} | ||||
| 	return serverVersion{ | ||||
| 		version: version{ | ||||
| 			major: res[0], | ||||
| 			minor: res[1], | ||||
| 			patch: res[2], | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								vendor/github.com/1Password/connect-sdk-go/onepassword/errors.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								vendor/github.com/1Password/connect-sdk-go/onepassword/errors.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| package onepassword | ||||
|  | ||||
| import "fmt" | ||||
|  | ||||
| // Error is an error returned by the Connect API. | ||||
| type Error struct { | ||||
| 	StatusCode int    `json:"status"` | ||||
| 	Message    string `json:"message"` | ||||
| } | ||||
|  | ||||
| func (e *Error) Error() string { | ||||
| 	return fmt.Sprintf("status %d: %s", e.StatusCode, e.Message) | ||||
| } | ||||
|  | ||||
| func (e *Error) Is(target error) bool { | ||||
| 	t, ok := target.(*Error) | ||||
| 	if !ok { | ||||
| 		return false | ||||
| 	} | ||||
| 	return t.Message == e.Message && t.StatusCode == e.StatusCode | ||||
| } | ||||
							
								
								
									
										49
									
								
								vendor/github.com/1Password/connect-sdk-go/onepassword/files.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								vendor/github.com/1Password/connect-sdk-go/onepassword/files.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| package onepassword | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| ) | ||||
|  | ||||
| type File struct { | ||||
| 	ID          string       `json:"id"` | ||||
| 	Name        string       `json:"name"` | ||||
| 	Section     *ItemSection `json:"section,omitempty"` | ||||
| 	Size        int          `json:"size"` | ||||
| 	ContentPath string       `json:"content_path"` | ||||
| 	content     []byte | ||||
| } | ||||
|  | ||||
| func (f *File) UnmarshalJSON(data []byte) error { | ||||
| 	var jsonFile struct { | ||||
| 		ID          string       `json:"id"` | ||||
| 		Name        string       `json:"name"` | ||||
| 		Section     *ItemSection `json:"section,omitempty"` | ||||
| 		Size        int          `json:"size"` | ||||
| 		ContentPath string       `json:"content_path"` | ||||
| 		Content     []byte       `json:"content,omitempty"` | ||||
| 	} | ||||
| 	if err := json.Unmarshal(data, &jsonFile); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	f.ID = jsonFile.ID | ||||
| 	f.Name = jsonFile.Name | ||||
| 	f.Section = jsonFile.Section | ||||
| 	f.Size = jsonFile.Size | ||||
| 	f.ContentPath = jsonFile.ContentPath | ||||
| 	f.content = jsonFile.Content | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Content returns the content of the file if they have been loaded and returns an error if they have not been loaded. | ||||
| // Use `client.GetFileContent(file *File)` instead to make sure the content is fetched automatically if not present. | ||||
| func (f *File) Content() ([]byte, error) { | ||||
| 	if f.content == nil { | ||||
| 		return nil, errors.New("file content not loaded") | ||||
| 	} | ||||
| 	return f.content, nil | ||||
| } | ||||
|  | ||||
| func (f *File) SetContent(content []byte) { | ||||
| 	f.content = content | ||||
| } | ||||
							
								
								
									
										4
									
								
								vendor/github.com/1Password/connect-sdk-go/onepassword/items.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/github.com/1Password/connect-sdk-go/onepassword/items.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -28,6 +28,7 @@ const ( | ||||
| 	Document             ItemCategory = "DOCUMENT" | ||||
| 	EmailAccount         ItemCategory = "EMAIL_ACCOUNT" | ||||
| 	SocialSecurityNumber ItemCategory = "SOCIAL_SECURITY_NUMBER" | ||||
| 	ApiCredential        ItemCategory = "API_CREDENTIAL" | ||||
| 	Custom               ItemCategory = "CUSTOM" | ||||
| ) | ||||
|  | ||||
| @@ -39,7 +40,7 @@ func (ic *ItemCategory) UnmarshalJSON(b []byte) error { | ||||
| 	switch category { | ||||
| 	case Login, Password, Server, Database, CreditCard, Membership, Passport, SoftwareLicense, | ||||
| 		OutdoorLicense, SecureNote, WirelessRouter, BankAccount, DriverLicense, Identity, RewardProgram, | ||||
| 		Document, EmailAccount, SocialSecurityNumber: | ||||
| 		Document, EmailAccount, SocialSecurityNumber, ApiCredential: | ||||
| 		*ic = category | ||||
| 	default: | ||||
| 		*ic = Custom | ||||
| @@ -64,6 +65,7 @@ type Item struct { | ||||
|  | ||||
| 	Sections []*ItemSection `json:"sections,omitempty"` | ||||
| 	Fields   []*ItemField   `json:"fields,omitempty"` | ||||
| 	Files    []*File        `json:"files,omitempty"` | ||||
|  | ||||
| 	LastEditedBy string    `json:"lastEditedBy,omitempty"` | ||||
| 	CreatedAt    time.Time `json:"createdAt,omitempty"` | ||||
|   | ||||
							
								
								
									
										172
									
								
								vendor/github.com/stretchr/testify/assert/assertion_compare.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										172
									
								
								vendor/github.com/stretchr/testify/assert/assertion_compare.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -13,12 +13,42 @@ const ( | ||||
| 	compareGreater | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	intType   = reflect.TypeOf(int(1)) | ||||
| 	int8Type  = reflect.TypeOf(int8(1)) | ||||
| 	int16Type = reflect.TypeOf(int16(1)) | ||||
| 	int32Type = reflect.TypeOf(int32(1)) | ||||
| 	int64Type = reflect.TypeOf(int64(1)) | ||||
|  | ||||
| 	uintType   = reflect.TypeOf(uint(1)) | ||||
| 	uint8Type  = reflect.TypeOf(uint8(1)) | ||||
| 	uint16Type = reflect.TypeOf(uint16(1)) | ||||
| 	uint32Type = reflect.TypeOf(uint32(1)) | ||||
| 	uint64Type = reflect.TypeOf(uint64(1)) | ||||
|  | ||||
| 	float32Type = reflect.TypeOf(float32(1)) | ||||
| 	float64Type = reflect.TypeOf(float64(1)) | ||||
|  | ||||
| 	stringType = reflect.TypeOf("") | ||||
| ) | ||||
|  | ||||
| func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 	obj1Value := reflect.ValueOf(obj1) | ||||
| 	obj2Value := reflect.ValueOf(obj2) | ||||
|  | ||||
| 	// throughout this switch we try and avoid calling .Convert() if possible, | ||||
| 	// as this has a pretty big performance impact | ||||
| 	switch kind { | ||||
| 	case reflect.Int: | ||||
| 		{ | ||||
| 			intobj1 := obj1.(int) | ||||
| 			intobj2 := obj2.(int) | ||||
| 			intobj1, ok := obj1.(int) | ||||
| 			if !ok { | ||||
| 				intobj1 = obj1Value.Convert(intType).Interface().(int) | ||||
| 			} | ||||
| 			intobj2, ok := obj2.(int) | ||||
| 			if !ok { | ||||
| 				intobj2 = obj2Value.Convert(intType).Interface().(int) | ||||
| 			} | ||||
| 			if intobj1 > intobj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -31,8 +61,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Int8: | ||||
| 		{ | ||||
| 			int8obj1 := obj1.(int8) | ||||
| 			int8obj2 := obj2.(int8) | ||||
| 			int8obj1, ok := obj1.(int8) | ||||
| 			if !ok { | ||||
| 				int8obj1 = obj1Value.Convert(int8Type).Interface().(int8) | ||||
| 			} | ||||
| 			int8obj2, ok := obj2.(int8) | ||||
| 			if !ok { | ||||
| 				int8obj2 = obj2Value.Convert(int8Type).Interface().(int8) | ||||
| 			} | ||||
| 			if int8obj1 > int8obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -45,8 +81,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Int16: | ||||
| 		{ | ||||
| 			int16obj1 := obj1.(int16) | ||||
| 			int16obj2 := obj2.(int16) | ||||
| 			int16obj1, ok := obj1.(int16) | ||||
| 			if !ok { | ||||
| 				int16obj1 = obj1Value.Convert(int16Type).Interface().(int16) | ||||
| 			} | ||||
| 			int16obj2, ok := obj2.(int16) | ||||
| 			if !ok { | ||||
| 				int16obj2 = obj2Value.Convert(int16Type).Interface().(int16) | ||||
| 			} | ||||
| 			if int16obj1 > int16obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -59,8 +101,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Int32: | ||||
| 		{ | ||||
| 			int32obj1 := obj1.(int32) | ||||
| 			int32obj2 := obj2.(int32) | ||||
| 			int32obj1, ok := obj1.(int32) | ||||
| 			if !ok { | ||||
| 				int32obj1 = obj1Value.Convert(int32Type).Interface().(int32) | ||||
| 			} | ||||
| 			int32obj2, ok := obj2.(int32) | ||||
| 			if !ok { | ||||
| 				int32obj2 = obj2Value.Convert(int32Type).Interface().(int32) | ||||
| 			} | ||||
| 			if int32obj1 > int32obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -73,8 +121,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Int64: | ||||
| 		{ | ||||
| 			int64obj1 := obj1.(int64) | ||||
| 			int64obj2 := obj2.(int64) | ||||
| 			int64obj1, ok := obj1.(int64) | ||||
| 			if !ok { | ||||
| 				int64obj1 = obj1Value.Convert(int64Type).Interface().(int64) | ||||
| 			} | ||||
| 			int64obj2, ok := obj2.(int64) | ||||
| 			if !ok { | ||||
| 				int64obj2 = obj2Value.Convert(int64Type).Interface().(int64) | ||||
| 			} | ||||
| 			if int64obj1 > int64obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -87,8 +141,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Uint: | ||||
| 		{ | ||||
| 			uintobj1 := obj1.(uint) | ||||
| 			uintobj2 := obj2.(uint) | ||||
| 			uintobj1, ok := obj1.(uint) | ||||
| 			if !ok { | ||||
| 				uintobj1 = obj1Value.Convert(uintType).Interface().(uint) | ||||
| 			} | ||||
| 			uintobj2, ok := obj2.(uint) | ||||
| 			if !ok { | ||||
| 				uintobj2 = obj2Value.Convert(uintType).Interface().(uint) | ||||
| 			} | ||||
| 			if uintobj1 > uintobj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -101,8 +161,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Uint8: | ||||
| 		{ | ||||
| 			uint8obj1 := obj1.(uint8) | ||||
| 			uint8obj2 := obj2.(uint8) | ||||
| 			uint8obj1, ok := obj1.(uint8) | ||||
| 			if !ok { | ||||
| 				uint8obj1 = obj1Value.Convert(uint8Type).Interface().(uint8) | ||||
| 			} | ||||
| 			uint8obj2, ok := obj2.(uint8) | ||||
| 			if !ok { | ||||
| 				uint8obj2 = obj2Value.Convert(uint8Type).Interface().(uint8) | ||||
| 			} | ||||
| 			if uint8obj1 > uint8obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -115,8 +181,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Uint16: | ||||
| 		{ | ||||
| 			uint16obj1 := obj1.(uint16) | ||||
| 			uint16obj2 := obj2.(uint16) | ||||
| 			uint16obj1, ok := obj1.(uint16) | ||||
| 			if !ok { | ||||
| 				uint16obj1 = obj1Value.Convert(uint16Type).Interface().(uint16) | ||||
| 			} | ||||
| 			uint16obj2, ok := obj2.(uint16) | ||||
| 			if !ok { | ||||
| 				uint16obj2 = obj2Value.Convert(uint16Type).Interface().(uint16) | ||||
| 			} | ||||
| 			if uint16obj1 > uint16obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -129,8 +201,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Uint32: | ||||
| 		{ | ||||
| 			uint32obj1 := obj1.(uint32) | ||||
| 			uint32obj2 := obj2.(uint32) | ||||
| 			uint32obj1, ok := obj1.(uint32) | ||||
| 			if !ok { | ||||
| 				uint32obj1 = obj1Value.Convert(uint32Type).Interface().(uint32) | ||||
| 			} | ||||
| 			uint32obj2, ok := obj2.(uint32) | ||||
| 			if !ok { | ||||
| 				uint32obj2 = obj2Value.Convert(uint32Type).Interface().(uint32) | ||||
| 			} | ||||
| 			if uint32obj1 > uint32obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -143,8 +221,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Uint64: | ||||
| 		{ | ||||
| 			uint64obj1 := obj1.(uint64) | ||||
| 			uint64obj2 := obj2.(uint64) | ||||
| 			uint64obj1, ok := obj1.(uint64) | ||||
| 			if !ok { | ||||
| 				uint64obj1 = obj1Value.Convert(uint64Type).Interface().(uint64) | ||||
| 			} | ||||
| 			uint64obj2, ok := obj2.(uint64) | ||||
| 			if !ok { | ||||
| 				uint64obj2 = obj2Value.Convert(uint64Type).Interface().(uint64) | ||||
| 			} | ||||
| 			if uint64obj1 > uint64obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -157,8 +241,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Float32: | ||||
| 		{ | ||||
| 			float32obj1 := obj1.(float32) | ||||
| 			float32obj2 := obj2.(float32) | ||||
| 			float32obj1, ok := obj1.(float32) | ||||
| 			if !ok { | ||||
| 				float32obj1 = obj1Value.Convert(float32Type).Interface().(float32) | ||||
| 			} | ||||
| 			float32obj2, ok := obj2.(float32) | ||||
| 			if !ok { | ||||
| 				float32obj2 = obj2Value.Convert(float32Type).Interface().(float32) | ||||
| 			} | ||||
| 			if float32obj1 > float32obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -171,8 +261,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.Float64: | ||||
| 		{ | ||||
| 			float64obj1 := obj1.(float64) | ||||
| 			float64obj2 := obj2.(float64) | ||||
| 			float64obj1, ok := obj1.(float64) | ||||
| 			if !ok { | ||||
| 				float64obj1 = obj1Value.Convert(float64Type).Interface().(float64) | ||||
| 			} | ||||
| 			float64obj2, ok := obj2.(float64) | ||||
| 			if !ok { | ||||
| 				float64obj2 = obj2Value.Convert(float64Type).Interface().(float64) | ||||
| 			} | ||||
| 			if float64obj1 > float64obj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -185,8 +281,14 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { | ||||
| 		} | ||||
| 	case reflect.String: | ||||
| 		{ | ||||
| 			stringobj1 := obj1.(string) | ||||
| 			stringobj2 := obj2.(string) | ||||
| 			stringobj1, ok := obj1.(string) | ||||
| 			if !ok { | ||||
| 				stringobj1 = obj1Value.Convert(stringType).Interface().(string) | ||||
| 			} | ||||
| 			stringobj2, ok := obj2.(string) | ||||
| 			if !ok { | ||||
| 				stringobj2 = obj2Value.Convert(stringType).Interface().(string) | ||||
| 			} | ||||
| 			if stringobj1 > stringobj2 { | ||||
| 				return compareGreater, true | ||||
| 			} | ||||
| @@ -240,6 +342,24 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter | ||||
| 	return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) | ||||
| } | ||||
|  | ||||
| // Positive asserts that the specified element is positive | ||||
| // | ||||
| //    assert.Positive(t, 1) | ||||
| //    assert.Positive(t, 1.23) | ||||
| func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	zero := reflect.Zero(reflect.TypeOf(e)) | ||||
| 	return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs) | ||||
| } | ||||
|  | ||||
| // Negative asserts that the specified element is negative | ||||
| // | ||||
| //    assert.Negative(t, -1) | ||||
| //    assert.Negative(t, -1.23) | ||||
| func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	zero := reflect.Zero(reflect.TypeOf(e)) | ||||
| 	return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs) | ||||
| } | ||||
|  | ||||
| func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
|   | ||||
							
								
								
									
										97
									
								
								vendor/github.com/stretchr/testify/assert/assertion_format.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										97
									
								
								vendor/github.com/stretchr/testify/assert/assertion_format.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -114,6 +114,24 @@ func Errorf(t TestingT, err error, msg string, args ...interface{}) bool { | ||||
| 	return Error(t, err, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. | ||||
| // This is a wrapper for errors.As. | ||||
| func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // ErrorIsf asserts that at least one of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return ErrorIs(t, err, target, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // Eventuallyf asserts that given condition will be met in waitFor time, | ||||
| // periodically checking target function each tick. | ||||
| // | ||||
| @@ -321,6 +339,54 @@ func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsil | ||||
| 	return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // IsDecreasingf asserts that the collection is decreasing | ||||
| // | ||||
| //    assert.IsDecreasingf(t, []int{2, 1, 0}, "error message %s", "formatted") | ||||
| //    assert.IsDecreasingf(t, []float{2, 1}, "error message %s", "formatted") | ||||
| //    assert.IsDecreasingf(t, []string{"b", "a"}, "error message %s", "formatted") | ||||
| func IsDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsDecreasing(t, object, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // IsIncreasingf asserts that the collection is increasing | ||||
| // | ||||
| //    assert.IsIncreasingf(t, []int{1, 2, 3}, "error message %s", "formatted") | ||||
| //    assert.IsIncreasingf(t, []float{1, 2}, "error message %s", "formatted") | ||||
| //    assert.IsIncreasingf(t, []string{"a", "b"}, "error message %s", "formatted") | ||||
| func IsIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsIncreasing(t, object, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // IsNonDecreasingf asserts that the collection is not decreasing | ||||
| // | ||||
| //    assert.IsNonDecreasingf(t, []int{1, 1, 2}, "error message %s", "formatted") | ||||
| //    assert.IsNonDecreasingf(t, []float{1, 2}, "error message %s", "formatted") | ||||
| //    assert.IsNonDecreasingf(t, []string{"a", "b"}, "error message %s", "formatted") | ||||
| func IsNonDecreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsNonDecreasing(t, object, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // IsNonIncreasingf asserts that the collection is not increasing | ||||
| // | ||||
| //    assert.IsNonIncreasingf(t, []int{2, 1, 1}, "error message %s", "formatted") | ||||
| //    assert.IsNonIncreasingf(t, []float{2, 1}, "error message %s", "formatted") | ||||
| //    assert.IsNonIncreasingf(t, []string{"b", "a"}, "error message %s", "formatted") | ||||
| func IsNonIncreasingf(t TestingT, object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsNonIncreasing(t, object, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // IsTypef asserts that the specified objects are of the same type. | ||||
| func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| @@ -375,6 +441,17 @@ func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args . | ||||
| 	return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // Negativef asserts that the specified element is negative | ||||
| // | ||||
| //    assert.Negativef(t, -1, "error message %s", "formatted") | ||||
| //    assert.Negativef(t, -1.23, "error message %s", "formatted") | ||||
| func Negativef(t TestingT, e interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return Negative(t, e, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // Neverf asserts that the given condition doesn't satisfy in waitFor time, | ||||
| // periodically checking the target function each tick. | ||||
| // | ||||
| @@ -476,6 +553,15 @@ func NotEqualValuesf(t TestingT, expected interface{}, actual interface{}, msg s | ||||
| 	return NotEqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // NotErrorIsf asserts that at none of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func NotErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return NotErrorIs(t, err, target, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // NotNilf asserts that the specified object is not nil. | ||||
| // | ||||
| //    assert.NotNilf(t, err, "error message %s", "formatted") | ||||
| @@ -572,6 +658,17 @@ func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg str | ||||
| 	return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // Positivef asserts that the specified element is positive | ||||
| // | ||||
| //    assert.Positivef(t, 1, "error message %s", "formatted") | ||||
| //    assert.Positivef(t, 1.23, "error message %s", "formatted") | ||||
| func Positivef(t TestingT, e interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return Positive(t, e, append([]interface{}{msg}, args...)...) | ||||
| } | ||||
|  | ||||
| // Regexpf asserts that a specified regexp matches a string. | ||||
| // | ||||
| //  assert.Regexpf(t, regexp.MustCompile("start"), "it's starting", "error message %s", "formatted") | ||||
|   | ||||
							
								
								
									
										194
									
								
								vendor/github.com/stretchr/testify/assert/assertion_forward.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										194
									
								
								vendor/github.com/stretchr/testify/assert/assertion_forward.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -204,6 +204,42 @@ func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool { | ||||
| 	return Error(a.t, err, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. | ||||
| // This is a wrapper for errors.As. | ||||
| func (a *Assertions) ErrorAs(err error, target interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return ErrorAs(a.t, err, target, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // ErrorAsf asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. | ||||
| // This is a wrapper for errors.As. | ||||
| func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return ErrorAsf(a.t, err, target, msg, args...) | ||||
| } | ||||
|  | ||||
| // ErrorIs asserts that at least one of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return ErrorIs(a.t, err, target, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // ErrorIsf asserts that at least one of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func (a *Assertions) ErrorIsf(err error, target error, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return ErrorIsf(a.t, err, target, msg, args...) | ||||
| } | ||||
|  | ||||
| // Errorf asserts that a function returned an error (i.e. not `nil`). | ||||
| // | ||||
| //   actualObj, err := SomeFunction() | ||||
| @@ -631,6 +667,102 @@ func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilo | ||||
| 	return InEpsilonf(a.t, expected, actual, epsilon, msg, args...) | ||||
| } | ||||
|  | ||||
| // IsDecreasing asserts that the collection is decreasing | ||||
| // | ||||
| //    a.IsDecreasing([]int{2, 1, 0}) | ||||
| //    a.IsDecreasing([]float{2, 1}) | ||||
| //    a.IsDecreasing([]string{"b", "a"}) | ||||
| func (a *Assertions) IsDecreasing(object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsDecreasing(a.t, object, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // IsDecreasingf asserts that the collection is decreasing | ||||
| // | ||||
| //    a.IsDecreasingf([]int{2, 1, 0}, "error message %s", "formatted") | ||||
| //    a.IsDecreasingf([]float{2, 1}, "error message %s", "formatted") | ||||
| //    a.IsDecreasingf([]string{"b", "a"}, "error message %s", "formatted") | ||||
| func (a *Assertions) IsDecreasingf(object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsDecreasingf(a.t, object, msg, args...) | ||||
| } | ||||
|  | ||||
| // IsIncreasing asserts that the collection is increasing | ||||
| // | ||||
| //    a.IsIncreasing([]int{1, 2, 3}) | ||||
| //    a.IsIncreasing([]float{1, 2}) | ||||
| //    a.IsIncreasing([]string{"a", "b"}) | ||||
| func (a *Assertions) IsIncreasing(object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsIncreasing(a.t, object, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // IsIncreasingf asserts that the collection is increasing | ||||
| // | ||||
| //    a.IsIncreasingf([]int{1, 2, 3}, "error message %s", "formatted") | ||||
| //    a.IsIncreasingf([]float{1, 2}, "error message %s", "formatted") | ||||
| //    a.IsIncreasingf([]string{"a", "b"}, "error message %s", "formatted") | ||||
| func (a *Assertions) IsIncreasingf(object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsIncreasingf(a.t, object, msg, args...) | ||||
| } | ||||
|  | ||||
| // IsNonDecreasing asserts that the collection is not decreasing | ||||
| // | ||||
| //    a.IsNonDecreasing([]int{1, 1, 2}) | ||||
| //    a.IsNonDecreasing([]float{1, 2}) | ||||
| //    a.IsNonDecreasing([]string{"a", "b"}) | ||||
| func (a *Assertions) IsNonDecreasing(object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsNonDecreasing(a.t, object, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // IsNonDecreasingf asserts that the collection is not decreasing | ||||
| // | ||||
| //    a.IsNonDecreasingf([]int{1, 1, 2}, "error message %s", "formatted") | ||||
| //    a.IsNonDecreasingf([]float{1, 2}, "error message %s", "formatted") | ||||
| //    a.IsNonDecreasingf([]string{"a", "b"}, "error message %s", "formatted") | ||||
| func (a *Assertions) IsNonDecreasingf(object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsNonDecreasingf(a.t, object, msg, args...) | ||||
| } | ||||
|  | ||||
| // IsNonIncreasing asserts that the collection is not increasing | ||||
| // | ||||
| //    a.IsNonIncreasing([]int{2, 1, 1}) | ||||
| //    a.IsNonIncreasing([]float{2, 1}) | ||||
| //    a.IsNonIncreasing([]string{"b", "a"}) | ||||
| func (a *Assertions) IsNonIncreasing(object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsNonIncreasing(a.t, object, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // IsNonIncreasingf asserts that the collection is not increasing | ||||
| // | ||||
| //    a.IsNonIncreasingf([]int{2, 1, 1}, "error message %s", "formatted") | ||||
| //    a.IsNonIncreasingf([]float{2, 1}, "error message %s", "formatted") | ||||
| //    a.IsNonIncreasingf([]string{"b", "a"}, "error message %s", "formatted") | ||||
| func (a *Assertions) IsNonIncreasingf(object interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return IsNonIncreasingf(a.t, object, msg, args...) | ||||
| } | ||||
|  | ||||
| // IsType asserts that the specified objects are of the same type. | ||||
| func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| @@ -739,6 +871,28 @@ func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...i | ||||
| 	return Lessf(a.t, e1, e2, msg, args...) | ||||
| } | ||||
|  | ||||
| // Negative asserts that the specified element is negative | ||||
| // | ||||
| //    a.Negative(-1) | ||||
| //    a.Negative(-1.23) | ||||
| func (a *Assertions) Negative(e interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return Negative(a.t, e, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // Negativef asserts that the specified element is negative | ||||
| // | ||||
| //    a.Negativef(-1, "error message %s", "formatted") | ||||
| //    a.Negativef(-1.23, "error message %s", "formatted") | ||||
| func (a *Assertions) Negativef(e interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return Negativef(a.t, e, msg, args...) | ||||
| } | ||||
|  | ||||
| // Never asserts that the given condition doesn't satisfy in waitFor time, | ||||
| // periodically checking the target function each tick. | ||||
| // | ||||
| @@ -941,6 +1095,24 @@ func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg str | ||||
| 	return NotEqualf(a.t, expected, actual, msg, args...) | ||||
| } | ||||
|  | ||||
| // NotErrorIs asserts that at none of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func (a *Assertions) NotErrorIs(err error, target error, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return NotErrorIs(a.t, err, target, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // NotErrorIsf asserts that at none of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func (a *Assertions) NotErrorIsf(err error, target error, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return NotErrorIsf(a.t, err, target, msg, args...) | ||||
| } | ||||
|  | ||||
| // NotNil asserts that the specified object is not nil. | ||||
| // | ||||
| //    a.NotNil(err) | ||||
| @@ -1133,6 +1305,28 @@ func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) b | ||||
| 	return Panicsf(a.t, f, msg, args...) | ||||
| } | ||||
|  | ||||
| // Positive asserts that the specified element is positive | ||||
| // | ||||
| //    a.Positive(1) | ||||
| //    a.Positive(1.23) | ||||
| func (a *Assertions) Positive(e interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return Positive(a.t, e, msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // Positivef asserts that the specified element is positive | ||||
| // | ||||
| //    a.Positivef(1, "error message %s", "formatted") | ||||
| //    a.Positivef(1.23, "error message %s", "formatted") | ||||
| func (a *Assertions) Positivef(e interface{}, msg string, args ...interface{}) bool { | ||||
| 	if h, ok := a.t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	return Positivef(a.t, e, msg, args...) | ||||
| } | ||||
|  | ||||
| // Regexp asserts that a specified regexp matches a string. | ||||
| // | ||||
| //  a.Regexp(regexp.MustCompile("start"), "it's starting") | ||||
|   | ||||
							
								
								
									
										81
									
								
								vendor/github.com/stretchr/testify/assert/assertion_order.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								vendor/github.com/stretchr/testify/assert/assertion_order.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| package assert | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"reflect" | ||||
| ) | ||||
|  | ||||
| // isOrdered checks that collection contains orderable elements. | ||||
| func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { | ||||
| 	objKind := reflect.TypeOf(object).Kind() | ||||
| 	if objKind != reflect.Slice && objKind != reflect.Array { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	objValue := reflect.ValueOf(object) | ||||
| 	objLen := objValue.Len() | ||||
|  | ||||
| 	if objLen <= 1 { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	value := objValue.Index(0) | ||||
| 	valueInterface := value.Interface() | ||||
| 	firstValueKind := value.Kind() | ||||
|  | ||||
| 	for i := 1; i < objLen; i++ { | ||||
| 		prevValue := value | ||||
| 		prevValueInterface := valueInterface | ||||
|  | ||||
| 		value = objValue.Index(i) | ||||
| 		valueInterface = value.Interface() | ||||
|  | ||||
| 		compareResult, isComparable := compare(prevValueInterface, valueInterface, firstValueKind) | ||||
|  | ||||
| 		if !isComparable { | ||||
| 			return Fail(t, fmt.Sprintf("Can not compare type \"%s\" and \"%s\"", reflect.TypeOf(value), reflect.TypeOf(prevValue)), msgAndArgs...) | ||||
| 		} | ||||
|  | ||||
| 		if !containsValue(allowedComparesResults, compareResult) { | ||||
| 			return Fail(t, fmt.Sprintf(failMessage, prevValue, value), msgAndArgs...) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| // IsIncreasing asserts that the collection is increasing | ||||
| // | ||||
| //    assert.IsIncreasing(t, []int{1, 2, 3}) | ||||
| //    assert.IsIncreasing(t, []float{1, 2}) | ||||
| //    assert.IsIncreasing(t, []string{"a", "b"}) | ||||
| func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) | ||||
| } | ||||
|  | ||||
| // IsNonIncreasing asserts that the collection is not increasing | ||||
| // | ||||
| //    assert.IsNonIncreasing(t, []int{2, 1, 1}) | ||||
| //    assert.IsNonIncreasing(t, []float{2, 1}) | ||||
| //    assert.IsNonIncreasing(t, []string{"b", "a"}) | ||||
| func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) | ||||
| } | ||||
|  | ||||
| // IsDecreasing asserts that the collection is decreasing | ||||
| // | ||||
| //    assert.IsDecreasing(t, []int{2, 1, 0}) | ||||
| //    assert.IsDecreasing(t, []float{2, 1}) | ||||
| //    assert.IsDecreasing(t, []string{"b", "a"}) | ||||
| func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) | ||||
| } | ||||
|  | ||||
| // IsNonDecreasing asserts that the collection is not decreasing | ||||
| // | ||||
| //    assert.IsNonDecreasing(t, []int{1, 1, 2}) | ||||
| //    assert.IsNonDecreasing(t, []float{1, 2}) | ||||
| //    assert.IsNonDecreasing(t, []string{"a", "b"}) | ||||
| func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) | ||||
| } | ||||
							
								
								
									
										83
									
								
								vendor/github.com/stretchr/testify/assert/assertions.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										83
									
								
								vendor/github.com/stretchr/testify/assert/assertions.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @@ -172,8 +172,8 @@ func isTest(name, prefix string) bool { | ||||
| 	if len(name) == len(prefix) { // "Test" is ok | ||||
| 		return true | ||||
| 	} | ||||
| 	rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) | ||||
| 	return !unicode.IsLower(rune) | ||||
| 	r, _ := utf8.DecodeRuneInString(name[len(prefix):]) | ||||
| 	return !unicode.IsLower(r) | ||||
| } | ||||
|  | ||||
| func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { | ||||
| @@ -1622,6 +1622,7 @@ var spewConfig = spew.ConfigState{ | ||||
| 	DisableCapacities:       true, | ||||
| 	SortKeys:                true, | ||||
| 	DisableMethods:          true, | ||||
| 	MaxDepth:                10, | ||||
| } | ||||
|  | ||||
| type tHelper interface { | ||||
| @@ -1693,3 +1694,81 @@ func Never(t TestingT, condition func() bool, waitFor time.Duration, tick time.D | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ErrorIs asserts that at least one of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	if errors.Is(err, target) { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	var expectedText string | ||||
| 	if target != nil { | ||||
| 		expectedText = target.Error() | ||||
| 	} | ||||
|  | ||||
| 	chain := buildErrorChainString(err) | ||||
|  | ||||
| 	return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+ | ||||
| 		"expected: %q\n"+ | ||||
| 		"in chain: %s", expectedText, chain, | ||||
| 	), msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // NotErrorIs asserts that at none of the errors in err's chain matches target. | ||||
| // This is a wrapper for errors.Is. | ||||
| func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	if !errors.Is(err, target) { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	var expectedText string | ||||
| 	if target != nil { | ||||
| 		expectedText = target.Error() | ||||
| 	} | ||||
|  | ||||
| 	chain := buildErrorChainString(err) | ||||
|  | ||||
| 	return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ | ||||
| 		"found: %q\n"+ | ||||
| 		"in chain: %s", expectedText, chain, | ||||
| 	), msgAndArgs...) | ||||
| } | ||||
|  | ||||
| // ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. | ||||
| // This is a wrapper for errors.As. | ||||
| func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool { | ||||
| 	if h, ok := t.(tHelper); ok { | ||||
| 		h.Helper() | ||||
| 	} | ||||
| 	if errors.As(err, target) { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	chain := buildErrorChainString(err) | ||||
|  | ||||
| 	return Fail(t, fmt.Sprintf("Should be in error chain:\n"+ | ||||
| 		"expected: %q\n"+ | ||||
| 		"in chain: %s", target, chain, | ||||
| 	), msgAndArgs...) | ||||
| } | ||||
|  | ||||
| func buildErrorChainString(err error) string { | ||||
| 	if err == nil { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	e := errors.Unwrap(err) | ||||
| 	chain := fmt.Sprintf("%q", err.Error()) | ||||
| 	for e != nil { | ||||
| 		chain += fmt.Sprintf("\n\t%q", e.Error()) | ||||
| 		e = errors.Unwrap(e) | ||||
| 	} | ||||
| 	return chain | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| # cloud.google.com/go v0.49.0 | ||||
| cloud.google.com/go/compute/metadata | ||||
| # github.com/1Password/connect-sdk-go v1.0.1 | ||||
| # github.com/1Password/connect-sdk-go v1.2.0 | ||||
| github.com/1Password/connect-sdk-go/connect | ||||
| github.com/1Password/connect-sdk-go/onepassword | ||||
| # github.com/Azure/go-autorest/autorest v0.9.3-0.20191028180845-3492b2aff503 | ||||
| @@ -110,7 +110,7 @@ github.com/prometheus/procfs/internal/fs | ||||
| github.com/prometheus/procfs/internal/util | ||||
| # github.com/spf13/pflag v1.0.5 | ||||
| github.com/spf13/pflag | ||||
| # github.com/stretchr/testify v1.6.1 | ||||
| # github.com/stretchr/testify v1.7.0 | ||||
| github.com/stretchr/testify/assert | ||||
| # github.com/uber/jaeger-client-go v2.25.0+incompatible | ||||
| github.com/uber/jaeger-client-go | ||||
|   | ||||
		Reference in New Issue
	
	Block a user