Compare commits
	
		
			59 Commits
		
	
	
		
			v0.5.0
			...
			4d36cd468f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 4d36cd468f | ||
| 6f099c4bf2 | |||
|  | aa015cc7ba | ||
|  | 2de9bbb0bf | ||
|  | 4505f3985c | ||
|  | 82b684e460 | ||
|  | 8ec698f50e | ||
|  | 9b8fe1d8ef | ||
|  | 516f2a34cf | ||
| 361d0866e9 | |||
| 9d4ade904e | |||
| 8c3714f7e0 | |||
| 36ae5cc602 | |||
| d908419b78 | |||
| 2d399ff8ce | |||
| c753737497 | |||
| 886fe3783d | |||
|  | 18a47f8ad2 | ||
|  | e405734e72 | ||
| 8bf4292991 | |||
|  | b149b26485 | ||
|  | 5263a811e1 | ||
|  | 4b59ff1aac | ||
|  | ad1cc9f646 | ||
| 0f518ab28d | |||
| 1bf2a24cf2 | |||
| a73e8ff982 | |||
|  | 54a4ffa212 | ||
| 16040bf87a | |||
|  | 9c1c7417e1 | ||
|  | 0f9ca0869c | ||
|  | 6fbf060044 | ||
|  | 3bb40cdcb4 | ||
|  | 219c9d0413 | ||
|  | 4f92bfe86a | ||
|  | 640333cfc7 | ||
|  | 6a907f149f | ||
|  | 3db74524ca | ||
|  | e49df1fb4d | ||
|  | bb3ca7573b | ||
| 097712c6c6 | |||
|  | 3845fd8045 | ||
|  | 3caacac98a | ||
|  | beeca5a6b6 | ||
|  | 2d4c8ec14b | ||
| 10cc864275 | |||
| 689a6e5bae | |||
|  | 4e23b67f5d | ||
|  | f4d05fdd0f | ||
|  | 48bc422974 | ||
|  | 41d4959422 | ||
|  | c2116c24ec | ||
|  | 67692b372f | ||
| 8a6219718a | |||
|  | a10f6b3c9a | ||
|  | 56657df85a | ||
|  | 6a324e66da | ||
|  | 6081374696 | ||
|  | a3cec12284 | 
							
								
								
									
										31
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,7 +8,9 @@ on: | ||||
| jobs: | ||||
|   release: | ||||
|     permissions: | ||||
|       id-token: write | ||||
|       contents: write | ||||
|       packages: write | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
| @@ -27,22 +29,43 @@ jobs: | ||||
|           version: v3.10.0 | ||||
|  | ||||
|       - name: Run chart-releaser | ||||
|         uses: helm/chart-releaser-action@v1.4.1 | ||||
|         uses: helm/chart-releaser-action@v1.5.0 | ||||
|         with: | ||||
|           charts_dir: charts | ||||
|         env: | ||||
|           CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" | ||||
|  | ||||
|       - name: Get app version from chart | ||||
|         uses: mikefarah/yq@v4.30.6 | ||||
|         uses: mikefarah/yq@v4.34.1 | ||||
|         id: app_version | ||||
|         with: | ||||
|           cmd: yq '.appVersion' charts/bitwarden-crd-operator/Chart.yaml | ||||
|  | ||||
|       - name: "GHCR Login" | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: lerentis | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|        | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|  | ||||
|       - name: "GHCR Build and Push" | ||||
|         id: docker_build | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           push: true | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           tags: ghcr.io/lerentis/bitwarden-crd-operator:${{ steps.app_version.outputs.result }} | ||||
|  | ||||
|       - name: Create SBOM | ||||
|         uses: anchore/sbom-action@v0 | ||||
|         with: | ||||
|           image: lerentis/bitwarden-crd-operator:${{ steps.app_version.outputs.result }} | ||||
|           image: ghcr.io/lerentis/bitwarden-crd-operator:${{ steps.app_version.outputs.result }} | ||||
|          | ||||
|       - name: Publish SBOM | ||||
|         uses: anchore/sbom-action/publish-sbom@v0 | ||||
| @@ -65,4 +88,4 @@ jobs: | ||||
|           file_glob: true | ||||
|           file: lerentis-bitwarden-crd-operator_*.spdx.json | ||||
|           tag:  ${{ steps.previoustag.outputs.tag }} | ||||
|           overwrite: false | ||||
|           overwrite: true | ||||
|   | ||||
							
								
								
									
										55
									
								
								.github/workflows/test-and-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								.github/workflows/test-and-lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| name: Lint and Test | ||||
|  | ||||
| on: pull_request | ||||
|  | ||||
| jobs: | ||||
|   lint-test: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|  | ||||
|       - name: Set up Helm | ||||
|         uses: azure/setup-helm@v3 | ||||
|         with: | ||||
|           version: v3.11.2 | ||||
|  | ||||
|       - uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: '3.9' | ||||
|           check-latest: true | ||||
|  | ||||
|       - name: Set up chart-testing | ||||
|         uses: helm/chart-testing-action@v2.4.0 | ||||
|  | ||||
|       - name: Run chart-testing (list-changed) | ||||
|         id: list-changed | ||||
|         run: | | ||||
|           changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }}) | ||||
|           if [[ -n "$changed" ]]; then | ||||
|             echo "changed=true" >> "$GITHUB_OUTPUT" | ||||
|           fi | ||||
|  | ||||
|       - name: Run chart-testing (lint) | ||||
|         if: steps.list-changed.outputs.changed == 'true' | ||||
|         run: ct lint --target-branch ${{ github.event.repository.default_branch }} | ||||
|  | ||||
|   pr-build: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|        | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|  | ||||
|       - name: "GHCR Build" | ||||
|         id: docker_build | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           push: false | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           tags: ghcr.io/lerentis/bitwarden-crd-operator:dev | ||||
|  | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -166,3 +166,5 @@ lib | ||||
| lib64 | ||||
|  | ||||
| myvalues.yaml | ||||
|  | ||||
| .vscode | ||||
							
								
								
									
										50
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,29 +1,45 @@ | ||||
| FROM alpine:latest as builder | ||||
| FROM alpine:3.18.0 | ||||
|  | ||||
| ARG BW_VERSION=2022.11.0 | ||||
| LABEL org.opencontainers.image.source=https://github.com/Lerentis/bitwarden-crd-operator | ||||
| LABEL org.opencontainers.image.description="Kubernetes Operator to create k8s secrets from bitwarden" | ||||
| LABEL org.opencontainers.image.licenses=MIT | ||||
|  | ||||
| RUN apk add wget unzip | ||||
| ARG PYTHON_VERSION=3.11.4-r0 | ||||
| ARG PIP_VERSION=23.1.2-r0 | ||||
| ARG GCOMPAT_VERSION=1.1.0-r1 | ||||
| ARG LIBCRYPTO_VERSION=3.1.0-r4 | ||||
| ARG BW_VERSION=2023.1.0 | ||||
|  | ||||
| RUN cd /tmp && wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip && \ | ||||
|     unzip /tmp/bw-linux-${BW_VERSION}.zip | ||||
|  | ||||
| FROM alpine:3.17 | ||||
|  | ||||
| ARG PYTHON_VERSION=3.10.9-r1 | ||||
| ARG PIP_VERSION=22.3.1-r1 | ||||
| ARG GCOMPAT_VERSION=1.1.0-r0 | ||||
|  | ||||
| COPY --from=builder /tmp/bw /usr/local/bin/bw | ||||
| COPY requirements.txt requirements.txt | ||||
| COPY requirements.txt /requirements.txt | ||||
|  | ||||
| RUN set -eux; \ | ||||
|     apk add --virtual build-dependencies wget unzip; \ | ||||
|     ARCH="$(apk --print-arch)"; \ | ||||
|     case "${ARCH}" in \ | ||||
|        aarch64|arm64) \ | ||||
|           apk add npm; \ | ||||
|           npm install -g @bitwarden/cli@${BW_VERSION}; \ | ||||
|          ;; \ | ||||
|        amd64|x86_64) \ | ||||
|           cd /tmp; \ | ||||
|           wget https://github.com/bitwarden/clients/releases/download/cli-v${BW_VERSION}/bw-linux-${BW_VERSION}.zip; \ | ||||
|           unzip /tmp/bw-linux-${BW_VERSION}.zip; \ | ||||
|           mv /tmp/bw /usr/local/bin/bw; \ | ||||
|           chmod +x /usr/local/bin/bw; \ | ||||
|          ;; \ | ||||
|        *) \ | ||||
|          echo "Unsupported arch: ${ARCH}"; \ | ||||
|          exit 1; \ | ||||
|          ;; \ | ||||
|     esac; \ | ||||
|     apk del --purge build-dependencies; \ | ||||
|     addgroup -S -g 1000 bw-operator; \ | ||||
|     adduser -S -D -u 1000 -G bw-operator bw-operator; \ | ||||
|     mkdir -p /home/bw-operator; \ | ||||
|     chown -R bw-operator /home/bw-operator; \ | ||||
|     chmod +x /usr/local/bin/bw; \ | ||||
|     apk add gcc musl-dev libstdc++ gcompat=${GCOMPAT_VERSION} python3=${PYTHON_VERSION} py-pip=${PIP_VERSION}; \ | ||||
|     pip install -r requirements.txt --no-warn-script-location; \ | ||||
|     apk add gcc musl-dev libstdc++ gcompat=${GCOMPAT_VERSION} python3=${PYTHON_VERSION} py3-pip=${PIP_VERSION} libcrypto3=${LIBCRYPTO_VERSION}; \ | ||||
|     pip install -r /requirements.txt --no-warn-script-location; \ | ||||
|     rm /requirements.txt; \ | ||||
|     apk del --purge gcc musl-dev libstdc++; | ||||
|  | ||||
| COPY --chown=bw-operator:bw-operator src /home/bw-operator | ||||
|   | ||||
| @@ -4,9 +4,9 @@ description: Deploy the Bitwarden CRD Operator | ||||
|  | ||||
| type: application | ||||
|  | ||||
| version: "v0.5.0" | ||||
| version: "v0.7.4" | ||||
|  | ||||
| appVersion: "0.5.0" | ||||
| appVersion: "0.6.4" | ||||
|  | ||||
| keywords: | ||||
|   - operator | ||||
| @@ -20,7 +20,7 @@ home: https://lerentis.github.io/bitwarden-crd-operator/ | ||||
| sources: | ||||
|   - https://github.com/Lerentis/bitwarden-crd-operator | ||||
|  | ||||
| kubeVersion: '>= 1.23.0-0' | ||||
| kubeVersion: ">= 1.23.0-0" | ||||
|  | ||||
| maintainers: | ||||
|   - name: lerentis | ||||
| @@ -93,13 +93,10 @@ annotations: | ||||
|                 enabled: true | ||||
|   artifacthub.io/license: MIT | ||||
|   artifacthub.io/operator: "true" | ||||
|   artifacthub.io/containsSecurityUpdates: "false" | ||||
|   artifacthub.io/changes: | | ||||
|     - kind: added | ||||
|       description: "Implemented update handling" | ||||
|     - kind: changed | ||||
|       description: "Changed default logging structure to json logging" | ||||
|     - kind: changed | ||||
|       description: "Secrets are periodically updated every 15 minutes" | ||||
|     - kind: fixed | ||||
|       description: "Fixed bitwarden installation" | ||||
|   artifacthub.io/images: | | ||||
|     - name: bitwarden-crd-operator | ||||
|       image: lerentis/bitwarden-crd-operator:0.5.0 | ||||
|       image: ghcr.io/lerentis/bitwarden-crd-operator:0.6.4 | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
| replicaCount: 1 | ||||
|  | ||||
| image: | ||||
|   repository: lerentis/bitwarden-crd-operator | ||||
|   repository: ghcr.io/lerentis/bitwarden-crd-operator | ||||
|   pullPolicy: IfNotPresent | ||||
|   # Overrides the image tag whose default is the chart appVersion. | ||||
|   # tag: "0.1.0" | ||||
| @@ -14,15 +14,15 @@ imagePullSecrets: [] | ||||
| nameOverride: "" | ||||
| fullnameOverride: "" | ||||
|  | ||||
| #env: | ||||
| #  - name: BW_HOST | ||||
| #    value: "define_it" | ||||
| #  - name: BW_CLIENTID | ||||
| #    value: "define_it" | ||||
| #  - name: BW_CLIENTSECRET | ||||
| #    value: "define_it" | ||||
| #  - name: BW_PASSWORD | ||||
| #    value: "define_id" | ||||
| # env: | ||||
| #   - name: BW_HOST | ||||
| #     value: "define_it" | ||||
| #   - name: BW_CLIENTID | ||||
| #     value: "define_it" | ||||
| #   - name: BW_CLIENTSECRET | ||||
| #     value: "define_it" | ||||
| #   - name: BW_PASSWORD | ||||
| #     value: "define_id" | ||||
|  | ||||
| externalConfigSecret: | ||||
|   enabled: false | ||||
|   | ||||
							
								
								
									
										14
									
								
								example.yaml
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								example.yaml
									
									
									
									
									
								
							| @@ -16,3 +16,17 @@ spec: | ||||
|   id: "88781348-c81c-4367-9801-550360c21295" | ||||
|   name: "test-secret" | ||||
|   namespace: "default" | ||||
| --- | ||||
| apiVersion: "lerentis.uploadfilter24.eu/v1beta4" | ||||
| kind: BitwardenSecret | ||||
| metadata: | ||||
|   name: test-scope | ||||
| spec: | ||||
|   content: | ||||
|     - element: | ||||
|         secretName: public_key | ||||
|         secretRef: pubKey  | ||||
|         secretScope: fields | ||||
|   id: "466fc4b0-ffca-4444-8d88-b59d4de3d928" | ||||
|   name: "test-scope" | ||||
|   namespace: "default" | ||||
| @@ -1,3 +1,3 @@ | ||||
| kopf==1.36.0 | ||||
| kubernetes==25.3.0 | ||||
| kopf==1.36.1 | ||||
| kubernetes==26.1.0 | ||||
| Jinja2==3.1.2 | ||||
|   | ||||
| @@ -1,20 +1,20 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import kopf | ||||
| import os | ||||
| import kopf | ||||
|  | ||||
| from utils.utils import command_wrapper, unlock_bw | ||||
|  | ||||
|  | ||||
| @kopf.on.startup() | ||||
| def bitwarden_signin(logger, **kwargs): | ||||
|     if 'BW_HOST' in os.environ: | ||||
|         try: | ||||
|             command_wrapper(f"config server {os.getenv('BW_HOST')}") | ||||
|         except: | ||||
|             logger.warn("Revieved none zero exit code from server config") | ||||
|             command_wrapper(logger, f"config server {os.getenv('BW_HOST')}") | ||||
|         except BaseException: | ||||
|             logger.warn("Received non-zero exit code from server config") | ||||
|             logger.warn("This is expected from startup") | ||||
|             pass | ||||
|     else: | ||||
|         logger.info(f"BW_HOST not set. Assuming SaaS installation") | ||||
|     command_wrapper("login --apikey") | ||||
|         logger.info("BW_HOST not set. Assuming SaaS installation") | ||||
|     command_wrapper(logger, "login --apikey") | ||||
|     unlock_bw(logger) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,14 @@ import json | ||||
|  | ||||
| from utils.utils import unlock_bw, get_secret_from_bitwarden | ||||
|  | ||||
| def create_dockerlogin(logger, secret, secret_json, username_ref, password_ref, registry): | ||||
|  | ||||
| def create_dockerlogin( | ||||
|         logger, | ||||
|         secret, | ||||
|         secret_json, | ||||
|         username_ref, | ||||
|         password_ref, | ||||
|         registry): | ||||
|     secret.type = "dockerconfigjson" | ||||
|     secret.data = {} | ||||
|     auths_dict = {} | ||||
| @@ -15,14 +22,19 @@ def create_dockerlogin(logger, secret, secret_json, username_ref, password_ref, | ||||
|     _username = secret_json["login"][username_ref] | ||||
|     logger.info(f"Creating login with username: {_username}") | ||||
|     _password = secret_json["login"][password_ref] | ||||
|     cred_field = str(base64.b64encode(f"{_username}:{_password}".encode("utf-8")), "utf-8") | ||||
|     cred_field = str( | ||||
|         base64.b64encode( | ||||
|             f"{_username}:{_password}".encode("utf-8")), | ||||
|         "utf-8") | ||||
|  | ||||
|     reg_auth_dict["auth"] = cred_field | ||||
|     registry_dict[registry] = reg_auth_dict | ||||
|     auths_dict["auths"] = registry_dict | ||||
|     secret.data[".dockerconfigjson"] = str(base64.b64encode(json.dumps(auths_dict).encode("utf-8")), "utf-8") | ||||
|     secret.data[".dockerconfigjson"] = str(base64.b64encode( | ||||
|         json.dumps(auths_dict).encode("utf-8")), "utf-8") | ||||
|     return secret | ||||
|  | ||||
|  | ||||
| @kopf.on.create('registry-credential.lerentis.uploadfilter24.eu') | ||||
| def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): | ||||
|     username_ref = spec.get('usernameRef') | ||||
| @@ -34,7 +46,7 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): | ||||
|  | ||||
|     unlock_bw(logger) | ||||
|     logger.info(f"Locking up secret with ID: {id}") | ||||
|     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||
|     secret_json_object = get_secret_from_bitwarden(logger, id) | ||||
|  | ||||
|     api = kubernetes.client.CoreV1Api() | ||||
|  | ||||
| @@ -43,18 +55,34 @@ def create_managed_registry_secret(spec, name, namespace, logger, **kwargs): | ||||
|         "managedObject": f"{namespace}/{name}" | ||||
|     } | ||||
|     secret = kubernetes.client.V1Secret() | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||
|     secret = create_dockerlogin(logger, secret, secret_json_object, username_ref, password_ref, registry)    | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta( | ||||
|         name=secret_name, annotations=annotations) | ||||
|     secret = create_dockerlogin( | ||||
|         logger, | ||||
|         secret, | ||||
|         secret_json_object["data"], | ||||
|         username_ref, | ||||
|         password_ref, | ||||
|         registry) | ||||
|  | ||||
|     obj = api.create_namespaced_secret( | ||||
|         secret_namespace, secret | ||||
|     ) | ||||
|  | ||||
|     logger.info(f"Registry Secret {secret_namespace}/{secret_name} has been created") | ||||
|     logger.info( | ||||
|         f"Registry Secret {secret_namespace}/{secret_name} has been created") | ||||
|  | ||||
|  | ||||
| @kopf.on.update('registry-credential.lerentis.uploadfilter24.eu') | ||||
| @kopf.timer('registry-credential.lerentis.uploadfilter24.eu', interval=900) | ||||
| def update_managed_registry_secret(spec, status, name, namespace, logger, body, **kwargs): | ||||
| def update_managed_registry_secret( | ||||
|         spec, | ||||
|         status, | ||||
|         name, | ||||
|         namespace, | ||||
|         logger, | ||||
|         body, | ||||
|         **kwargs): | ||||
|  | ||||
|     username_ref = spec.get('usernameRef') | ||||
|     password_ref = spec.get('passwordRef') | ||||
| @@ -63,9 +91,34 @@ def update_managed_registry_secret(spec, status, name, namespace, logger, body, | ||||
|     secret_name = spec.get('name') | ||||
|     secret_namespace = spec.get('namespace') | ||||
|  | ||||
|     old_config = None | ||||
|     old_secret_name = None | ||||
|     old_secret_namespace = None | ||||
|     if 'kopf.zalando.org/last-handled-configuration' in body.metadata.annotations: | ||||
|         old_config = json.loads( | ||||
|             body.metadata.annotations['kopf.zalando.org/last-handled-configuration']) | ||||
|         old_secret_name = old_config['spec'].get('name') | ||||
|         old_secret_namespace = old_config['spec'].get('namespace') | ||||
|     secret_name = spec.get('name') | ||||
|     secret_namespace = spec.get('namespace') | ||||
|  | ||||
|     if old_config is not None and ( | ||||
|             old_secret_name != secret_name or old_secret_namespace != secret_namespace): | ||||
|         # If the name of the secret or the namespace of the secret is different | ||||
|         # We have to delete the secret an recreate it | ||||
|         logger.info("Secret name or namespace changed, let's recreate it") | ||||
|         delete_managed_secret( | ||||
|             old_config['spec'], | ||||
|             name, | ||||
|             namespace, | ||||
|             logger, | ||||
|             **kwargs) | ||||
|         create_managed_registry_secret(spec, name, namespace, logger, **kwargs) | ||||
|         return | ||||
|  | ||||
|     unlock_bw(logger) | ||||
|     logger.info(f"Locking up secret with ID: {id}") | ||||
|     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||
|     secret_json_object = get_secret_from_bitwarden(logger, id) | ||||
|  | ||||
|     api = kubernetes.client.CoreV1Api() | ||||
|  | ||||
| @@ -74,15 +127,23 @@ def update_managed_registry_secret(spec, status, name, namespace, logger, body, | ||||
|         "managedObject": f"{namespace}/{name}" | ||||
|     } | ||||
|     secret = kubernetes.client.V1Secret() | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||
|     secret = create_dockerlogin(logger, secret, secret_json_object, username_ref, password_ref, registry) | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta( | ||||
|         name=secret_name, annotations=annotations) | ||||
|     secret = create_dockerlogin( | ||||
|         logger, | ||||
|         secret, | ||||
|         secret_json_object["data"], | ||||
|         username_ref, | ||||
|         password_ref, | ||||
|         registry) | ||||
|     try: | ||||
|         obj = api.replace_namespaced_secret( | ||||
|             name=secret_name, | ||||
|             body=secret, | ||||
|             namespace="{}".format(secret_namespace)) | ||||
|         logger.info(f"Secret {secret_namespace}/{secret_name} has been updated") | ||||
|     except: | ||||
|         logger.info( | ||||
|             f"Secret {secret_namespace}/{secret_name} has been updated") | ||||
|     except BaseException: | ||||
|         logger.warn( | ||||
|             f"Could not update secret {secret_namespace}/{secret_name}!") | ||||
|  | ||||
| @@ -95,6 +156,8 @@ def delete_managed_secret(spec, name, namespace, logger, **kwargs): | ||||
|  | ||||
|     try: | ||||
|         api.delete_namespaced_secret(secret_name, secret_namespace) | ||||
|         logger.info(f"Secret {secret_namespace}/{secret_name} has been deleted") | ||||
|     except: | ||||
|         logger.warn(f"Could not delete secret {secret_namespace}/{secret_name}!") | ||||
|         logger.info( | ||||
|             f"Secret {secret_namespace}/{secret_name} has been deleted") | ||||
|     except BaseException: | ||||
|         logger.warn( | ||||
|             f"Could not delete secret {secret_namespace}/{secret_name}!") | ||||
|   | ||||
							
								
								
									
										54
									
								
								src/kv.py
									
									
									
									
									
								
							
							
						
						
									
										54
									
								
								src/kv.py
									
									
									
									
									
								
							| @@ -19,11 +19,19 @@ def create_kv(secret, secret_json, content_def): | ||||
|                 if key == "secretScope": | ||||
|                     _secret_scope = value | ||||
|             if _secret_scope == "login": | ||||
|                 value = parse_login_scope(secret_json, _secret_key) | ||||
|                 if value is None: | ||||
|                     raise Exception( | ||||
|                         f"Field {_secret_key} has no value in bitwarden secret") | ||||
|                 secret.data[_secret_ref] = str(base64.b64encode( | ||||
|                     parse_login_scope(secret_json, _secret_key).encode("utf-8")), "utf-8") | ||||
|                     value.encode("utf-8")), "utf-8") | ||||
|             if _secret_scope == "fields": | ||||
|                 value = parse_fields_scope(secret_json, _secret_key) | ||||
|                 if value is None: | ||||
|                     raise Exception( | ||||
|                         f"Field {_secret_key} has no value in bitwarden secret") | ||||
|                 secret.data[_secret_ref] = str(base64.b64encode( | ||||
|                     parse_fields_scope(secret_json, _secret_key).encode("utf-8")), "utf-8") | ||||
|                     value.encode("utf-8")), "utf-8") | ||||
|     return secret | ||||
|  | ||||
|  | ||||
| @@ -37,7 +45,7 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): | ||||
|  | ||||
|     unlock_bw(logger) | ||||
|     logger.info(f"Locking up secret with ID: {id}") | ||||
|     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||
|     secret_json_object = get_secret_from_bitwarden(logger, id) | ||||
|  | ||||
|     api = kubernetes.client.CoreV1Api() | ||||
|  | ||||
| @@ -60,16 +68,45 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): | ||||
|  | ||||
| @kopf.on.update('bitwarden-secret.lerentis.uploadfilter24.eu') | ||||
| @kopf.timer('bitwarden-secret.lerentis.uploadfilter24.eu', interval=900) | ||||
| def update_managed_secret(spec, status, name, namespace, logger, body, **kwargs): | ||||
| def update_managed_secret( | ||||
|         spec, | ||||
|         status, | ||||
|         name, | ||||
|         namespace, | ||||
|         logger, | ||||
|         body, | ||||
|         **kwargs): | ||||
|  | ||||
|     content_def = body['spec']['content'] | ||||
|     id = spec.get('id') | ||||
|     old_config = None | ||||
|     old_secret_name = None | ||||
|     old_secret_namespace = None | ||||
|     if 'kopf.zalando.org/last-handled-configuration' in body.metadata.annotations: | ||||
|         old_config = json.loads( | ||||
|             body.metadata.annotations['kopf.zalando.org/last-handled-configuration']) | ||||
|         old_secret_name = old_config['spec'].get('name') | ||||
|         old_secret_namespace = old_config['spec'].get('namespace') | ||||
|     secret_name = spec.get('name') | ||||
|     secret_namespace = spec.get('namespace') | ||||
|  | ||||
|     if old_config is not None and ( | ||||
|             old_secret_name != secret_name or old_secret_namespace != secret_namespace): | ||||
|         # If the name of the secret or the namespace of the secret is different | ||||
|         # We have to delete the secret an recreate it | ||||
|         logger.info("Secret name or namespace changed, let's recreate it") | ||||
|         delete_managed_secret( | ||||
|             old_config['spec'], | ||||
|             name, | ||||
|             namespace, | ||||
|             logger, | ||||
|             **kwargs) | ||||
|         create_managed_secret(spec, name, namespace, logger, body, **kwargs) | ||||
|         return | ||||
|  | ||||
|     unlock_bw(logger) | ||||
|     logger.info(f"Locking up secret with ID: {id}") | ||||
|     secret_json_object = json.loads(get_secret_from_bitwarden(id)) | ||||
|     secret_json_object = get_secret_from_bitwarden(logger, id) | ||||
|  | ||||
|     api = kubernetes.client.CoreV1Api() | ||||
|  | ||||
| @@ -88,8 +125,9 @@ def update_managed_secret(spec, status, name, namespace, logger, body, **kwargs) | ||||
|             name=secret_name, | ||||
|             body=secret, | ||||
|             namespace="{}".format(secret_namespace)) | ||||
|         logger.info(f"Secret {secret_namespace}/{secret_name} has been updated") | ||||
|     except: | ||||
|         logger.info( | ||||
|             f"Secret {secret_namespace}/{secret_name} has been updated") | ||||
|     except BaseException: | ||||
|         logger.warn( | ||||
|             f"Could not update secret {secret_namespace}/{secret_name}!") | ||||
|  | ||||
| @@ -104,6 +142,6 @@ def delete_managed_secret(spec, name, namespace, logger, **kwargs): | ||||
|         api.delete_namespaced_secret(secret_name, secret_namespace) | ||||
|         logger.info( | ||||
|             f"Secret {secret_namespace}/{secret_name} has been deleted") | ||||
|     except: | ||||
|     except BaseException: | ||||
|         logger.warn( | ||||
|             f"Could not delete secret {secret_namespace}/{secret_name}!") | ||||
|   | ||||
| @@ -2,8 +2,9 @@ import json | ||||
|  | ||||
| from utils.utils import get_secret_from_bitwarden, parse_fields_scope, parse_login_scope | ||||
|  | ||||
|  | ||||
| def bitwarden_lookup(id, scope, field): | ||||
|     _secret_json = json.loads(get_secret_from_bitwarden(id)) | ||||
|     _secret_json = get_secret_from_bitwarden(None, id) | ||||
|     if scope == "login": | ||||
|         return parse_login_scope(_secret_json, field) | ||||
|     if scope == "fields": | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import kopf | ||||
| import base64 | ||||
| import kubernetes | ||||
| import json | ||||
|  | ||||
| from utils.utils import unlock_bw | ||||
| from lookups.bitwarden_lookup import bitwarden_lookup | ||||
| @@ -11,17 +12,23 @@ lookup_func_dict = { | ||||
|     "bitwarden_lookup": bitwarden_lookup, | ||||
| } | ||||
|  | ||||
|  | ||||
| def render_template(template): | ||||
|     jinja_template = Environment(loader=BaseLoader()).from_string(template) | ||||
|     jinja_template.globals.update(lookup_func_dict) | ||||
|     return jinja_template.render() | ||||
|  | ||||
|  | ||||
| def create_template_secret(secret, filename, template): | ||||
|     secret.type = "Opaque" | ||||
|     secret.data = {} | ||||
|     secret.data[filename] = str(base64.b64encode(render_template(template).encode("utf-8")), "utf-8") | ||||
|     secret.data[filename] = str( | ||||
|         base64.b64encode( | ||||
|             render_template(template).encode("utf-8")), | ||||
|         "utf-8") | ||||
|     return secret | ||||
|  | ||||
|  | ||||
| @kopf.on.create('bitwarden-template.lerentis.uploadfilter24.eu') | ||||
| def create_managed_secret(spec, name, namespace, logger, body, **kwargs): | ||||
|  | ||||
| @@ -39,7 +46,8 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): | ||||
|         "managedObject": f"{namespace}/{name}" | ||||
|     } | ||||
|     secret = kubernetes.client.V1Secret() | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta( | ||||
|         name=secret_name, annotations=annotations) | ||||
|     secret = create_template_secret(secret, filename, template) | ||||
|  | ||||
|     obj = api.create_namespaced_secret( | ||||
| @@ -48,15 +56,48 @@ def create_managed_secret(spec, name, namespace, logger, body, **kwargs): | ||||
|  | ||||
|     logger.info(f"Secret {secret_namespace}/{secret_name} has been created") | ||||
|  | ||||
|  | ||||
| @kopf.on.update('bitwarden-template.lerentis.uploadfilter24.eu') | ||||
| @kopf.timer('bitwarden-template.lerentis.uploadfilter24.eu', interval=900) | ||||
| def update_managed_secret(spec, status, name, namespace, logger, body, **kwargs): | ||||
| def update_managed_secret( | ||||
|         spec, | ||||
|         status, | ||||
|         name, | ||||
|         namespace, | ||||
|         logger, | ||||
|         body, | ||||
|         **kwargs): | ||||
|  | ||||
|     template = spec.get('template') | ||||
|     filename = spec.get('filename') | ||||
|     secret_name = spec.get('name') | ||||
|     secret_namespace = spec.get('namespace') | ||||
|  | ||||
|     old_config = None | ||||
|     old_secret_name = None | ||||
|     old_secret_namespace = None | ||||
|     if 'kopf.zalando.org/last-handled-configuration' in body.metadata.annotations: | ||||
|         old_config = json.loads( | ||||
|             body.metadata.annotations['kopf.zalando.org/last-handled-configuration']) | ||||
|         old_secret_name = old_config['spec'].get('name') | ||||
|         old_secret_namespace = old_config['spec'].get('namespace') | ||||
|     secret_name = spec.get('name') | ||||
|     secret_namespace = spec.get('namespace') | ||||
|  | ||||
|     if old_config is not None and ( | ||||
|             old_secret_name != secret_name or old_secret_namespace != secret_namespace): | ||||
|         # If the name of the secret or the namespace of the secret is different | ||||
|         # We have to delete the secret an recreate it | ||||
|         logger.info("Secret name or namespace changed, let's recreate it") | ||||
|         delete_managed_secret( | ||||
|             old_config['spec'], | ||||
|             name, | ||||
|             namespace, | ||||
|             logger, | ||||
|             **kwargs) | ||||
|         create_managed_secret(spec, name, namespace, logger, body, **kwargs) | ||||
|         return | ||||
|  | ||||
|     unlock_bw(logger) | ||||
|  | ||||
|     api = kubernetes.client.CoreV1Api() | ||||
| @@ -66,7 +107,8 @@ def update_managed_secret(spec, status, name, namespace, logger, body, **kwargs) | ||||
|         "managedObject": f"{namespace}/{name}" | ||||
|     } | ||||
|     secret = kubernetes.client.V1Secret() | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta(name=secret_name, annotations=annotations) | ||||
|     secret.metadata = kubernetes.client.V1ObjectMeta( | ||||
|         name=secret_name, annotations=annotations) | ||||
|     secret = create_template_secret(secret, filename, template) | ||||
|  | ||||
|     try: | ||||
| @@ -74,11 +116,13 @@ def update_managed_secret(spec, status, name, namespace, logger, body, **kwargs) | ||||
|             name=secret_name, | ||||
|             body=secret, | ||||
|             namespace="{}".format(secret_namespace)) | ||||
|         logger.info(f"Secret {secret_namespace}/{secret_name} has been updated") | ||||
|     except: | ||||
|         logger.info( | ||||
|             f"Secret {secret_namespace}/{secret_name} has been updated") | ||||
|     except BaseException: | ||||
|         logger.warn( | ||||
|             f"Could not update secret {secret_namespace}/{secret_name}!") | ||||
|  | ||||
|  | ||||
| @kopf.on.delete('bitwarden-template.lerentis.uploadfilter24.eu') | ||||
| def delete_managed_secret(spec, name, namespace, logger, **kwargs): | ||||
|     secret_name = spec.get('name') | ||||
| @@ -87,6 +131,8 @@ def delete_managed_secret(spec, name, namespace, logger, **kwargs): | ||||
|  | ||||
|     try: | ||||
|         api.delete_namespaced_secret(secret_name, secret_namespace) | ||||
|         logger.info(f"Secret {secret_namespace}/{secret_name} has been deleted") | ||||
|     except: | ||||
|         logger.warn(f"Could not delete secret {secret_namespace}/{secret_name}!") | ||||
|         logger.info( | ||||
|             f"Secret {secret_namespace}/{secret_name} has been deleted") | ||||
|     except BaseException: | ||||
|         logger.warn( | ||||
|             f"Could not delete secret {secret_namespace}/{secret_name}!") | ||||
|   | ||||
| @@ -1,30 +1,53 @@ | ||||
| import os | ||||
| import json | ||||
| import subprocess | ||||
|  | ||||
|  | ||||
| class BitwardenCommandException(Exception): | ||||
|     pass | ||||
|  | ||||
| def get_secret_from_bitwarden(id): | ||||
|     return command_wrapper(command=f"get item {id}") | ||||
|  | ||||
| def get_secret_from_bitwarden(logger, id): | ||||
|     return command_wrapper(logger, command=f"get item {id}") | ||||
|  | ||||
|  | ||||
| def unlock_bw(logger): | ||||
|     token_output = command_wrapper("unlock --passwordenv BW_PASSWORD") | ||||
|     tokens = token_output.split('"')[1::2] | ||||
|     os.environ["BW_SESSION"] = tokens[1] | ||||
|     status_output = command_wrapper(logger, "status", False) | ||||
|     status = status_output['data']['template']['status'] | ||||
|     if status == 'unlocked': | ||||
|         logger.info("Already unlocked") | ||||
|         return | ||||
|     token_output = command_wrapper(logger, "unlock --passwordenv BW_PASSWORD") | ||||
|     os.environ["BW_SESSION"] = token_output["data"]["raw"] | ||||
|     logger.info("Signin successful. Session exported") | ||||
|  | ||||
| def command_wrapper(command): | ||||
|  | ||||
| def command_wrapper(logger, command, use_success: bool = True): | ||||
|     system_env = dict(os.environ) | ||||
|     sp = subprocess.Popen([f"bw {command}"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, shell=True, env=system_env) | ||||
|     sp = subprocess.Popen( | ||||
|         [f"bw --response {command}"], | ||||
|         stdout=subprocess.PIPE, | ||||
|         stderr=subprocess.PIPE, | ||||
|         close_fds=True, | ||||
|         shell=True, | ||||
|         env=system_env) | ||||
|     out, err = sp.communicate() | ||||
|     if err: | ||||
|         raise BitwardenCommandException(err) | ||||
|     return out.decode(encoding='UTF-8') | ||||
|     if "DEBUG" in system_env: | ||||
|         logger.info(out.decode(encoding='UTF-8')) | ||||
|     resp = json.loads(out.decode(encoding='UTF-8')) | ||||
|     if resp["success"] != None and (not use_success or (use_success and resp["success"] == True)): | ||||
|         return resp | ||||
|     logger.warn(resp) | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def parse_login_scope(secret_json, key): | ||||
|     return secret_json["login"][key] | ||||
|     return secret_json["data"]["login"][key] | ||||
|  | ||||
|  | ||||
| def parse_fields_scope(secret_json, key): | ||||
|     for entry in secret_json["fields"]: | ||||
|     if "fields" not in secret_json["data"]: | ||||
|         return None | ||||
|     for entry in secret_json["data"]["fields"]: | ||||
|         if entry['name'] == key: | ||||
|             return entry['value'] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user