Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package k8s-sidecar for openSUSE:Factory checked in at 2021-01-19 16:01:33 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/k8s-sidecar (Old) and /work/SRC/openSUSE:Factory/.k8s-sidecar.new.28504 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "k8s-sidecar" Tue Jan 19 16:01:33 2021 rev:2 rq:861700 version:0.1.144 Changes: -------- --- /work/SRC/openSUSE:Factory/k8s-sidecar/k8s-sidecar.changes 2020-06-29 21:16:24.965422116 +0200 +++ /work/SRC/openSUSE:Factory/.k8s-sidecar.new.28504/k8s-sidecar.changes 2021-01-19 16:01:51.227349078 +0100 @@ -1,0 +2,13 @@ +Fri Jan 08 12:39:24 UTC 2021 - rbr...@suse.com + +- Update to version 0.1.144: + * Add support for configmaps with binary data (#96) + * #98: only perform request if files where changed (#100) + * update Kubernetes library to v12.0.0 (#97) + * Improve versioning and tagging releases (#95) + * add kubeconfig + * Update libs (#90) + * Fix custom SLEEP_TIME + * Add multi-arch builds with docker buildx (#86) + +------------------------------------------------------------------- Old: ---- k8s-sidecar-0.1.75.tar.gz New: ---- k8s-sidecar-0.1.144.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ k8s-sidecar.spec ++++++ --- /var/tmp/diff_new_pack.zhvuDC/_old 2021-01-19 16:01:51.943350161 +0100 +++ /var/tmp/diff_new_pack.zhvuDC/_new 2021-01-19 16:01:51.943350161 +0100 @@ -1,7 +1,7 @@ # # spec file for package k8s-sidecar # -# Copyright (c) 2020 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2021 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -20,14 +20,14 @@ %define import_path github.com/kiwigrid/k8s-sidecar Name: k8s-sidecar -Version: 0.1.75 +Version: 0.1.144 Release: 0 Summary: Collect kubernetes cluster configmaps and store it License: MIT Group: Development/Languages/Python URL: https://github.com/kiwigrid/k8s-sidecar Source0: %{name}-%{version}.tar.gz -BuildArchitectures: noarch +BuildArch: noarch Requires: python3 Requires: python3-kubernetes Requires: python3-requests ++++++ _service ++++++ --- /var/tmp/diff_new_pack.zhvuDC/_old 2021-01-19 16:01:51.983350221 +0100 +++ /var/tmp/diff_new_pack.zhvuDC/_new 2021-01-19 16:01:51.983350221 +0100 @@ -3,8 +3,8 @@ <param name="url">https://github.com/kiwigrid/k8s-sidecar.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="versionformat">0.1.75</param> - <param name="revision">0.1.75</param> + <param name="versionformat">0.1.144</param> + <param name="revision">0.1.144</param> <param name="changesgenerate">enable</param> </service> <service name="tar" mode="disabled"/> ++++++ k8s-sidecar-0.1.75.tar.gz -> k8s-sidecar-0.1.144.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/.circleci/config.yml new/k8s-sidecar-0.1.144/.circleci/config.yml --- old/k8s-sidecar-0.1.75/.circleci/config.yml 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/.circleci/config.yml 2020-12-12 10:59:23.000000000 +0100 @@ -3,7 +3,7 @@ jobs: build: docker: - - image: circleci/python:3.7.4 + - image: circleci/python:3.7.8 working_directory: ~/repo @@ -38,24 +38,35 @@ # destination: test-reports - setup_remote_docker: - docker_layer_caching: true + version: 19.03.12 - run: | - TAG=0.1.$CIRCLE_BUILD_NUM - echo "$TAG" > /tmp/VERSION - docker build -t kiwigrid/k8s-sidecar-build:$TAG . + if [[ -z "${CIRCLE_TAG}" ]]; then + TAG=0.1.$CIRCLE_BUILD_NUM + else + TAG="${CIRCLE_TAG}" + fi + echo "$TAG" | tee /tmp/VERSION + mkdir -vp ~/.docker/cli-plugins/ + curl --silent -L "https://github.com/docker/buildx/releases/download/v0.4.1/buildx-v0.4.1.linux-amd64" > ~/.docker/cli-plugins/docker-buildx + chmod a+x ~/.docker/cli-plugins/docker-buildx + docker buildx version + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + docker context create buildcontext + docker buildx create buildcontext --use docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD - docker push kiwigrid/k8s-sidecar-build:$TAG + docker buildx build --push -t ${CIRCLE_PROJECT_USERNAME}/k8s-sidecar-build:$TAG --platform linux/amd64,linux/arm64,linux/arm/v7 . + - persist_to_workspace: root: /tmp paths: - VERSION - test-k8s-1-13: + test-k8s-1-14: machine: true environment: - KIND_VERSION: v0.7.0 - K8S_VERSION: v1.13.10 + KIND_VERSION: v0.8.1 + K8S_VERSION: v1.14.10 steps: - attach_workspace: at: /tmp/ @@ -63,17 +74,18 @@ - run: | VERSION=`cat /tmp/VERSION` sed -i 's/SIDECAR_VERSION/'"$VERSION"'/g' .circleci/test/sidecar.yaml + sed -i 's/CIRCLE_PROJECT_USERNAME/'"$CIRCLE_PROJECT_USERNAME"'/g' .circleci/test/sidecar.yaml cat .circleci/test/sidecar.yaml - run: name: test-in-cluster command: .circleci/test-in-cluster.sh no_output_timeout: 3600 - test-k8s-1-14: + test-k8s-1-15: machine: true environment: - KIND_VERSION: v0.7.0 - K8S_VERSION: v1.14.6 + KIND_VERSION: v0.8.1 + K8S_VERSION: v1.15.11 steps: - attach_workspace: at: /tmp/ @@ -81,17 +93,18 @@ - run: | VERSION=`cat /tmp/VERSION` sed -i 's/SIDECAR_VERSION/'"$VERSION"'/g' .circleci/test/sidecar.yaml + sed -i 's/CIRCLE_PROJECT_USERNAME/'"$CIRCLE_PROJECT_USERNAME"'/g' .circleci/test/sidecar.yaml cat .circleci/test/sidecar.yaml - run: name: test-in-cluster command: .circleci/test-in-cluster.sh no_output_timeout: 3600 - test-k8s-1-15: + test-k8s-1-16: machine: true environment: - KIND_VERSION: v0.7.0 - K8S_VERSION: v1.15.3 + KIND_VERSION: v0.8.1 + K8S_VERSION: v1.16.9 steps: - attach_workspace: at: /tmp/ @@ -99,17 +112,18 @@ - run: | VERSION=`cat /tmp/VERSION` sed -i 's/SIDECAR_VERSION/'"$VERSION"'/g' .circleci/test/sidecar.yaml + sed -i 's/CIRCLE_PROJECT_USERNAME/'"$CIRCLE_PROJECT_USERNAME"'/g' .circleci/test/sidecar.yaml cat .circleci/test/sidecar.yaml - run: name: test-in-cluster command: .circleci/test-in-cluster.sh no_output_timeout: 3600 - test-k8s-1-16: + test-k8s-1-17: machine: true environment: - KIND_VERSION: v0.7.0 - K8S_VERSION: v1.16.2 + KIND_VERSION: v0.8.1 + K8S_VERSION: v1.17.5 steps: - attach_workspace: at: /tmp/ @@ -117,6 +131,45 @@ - run: | VERSION=`cat /tmp/VERSION` sed -i 's/SIDECAR_VERSION/'"$VERSION"'/g' .circleci/test/sidecar.yaml + sed -i 's/CIRCLE_PROJECT_USERNAME/'"$CIRCLE_PROJECT_USERNAME"'/g' .circleci/test/sidecar.yaml + cat .circleci/test/sidecar.yaml + - run: + name: test-in-cluster + command: .circleci/test-in-cluster.sh + no_output_timeout: 3600 + + test-k8s-1-18: + machine: true + environment: + KIND_VERSION: v0.8.1 + K8S_VERSION: v1.18.8 + steps: + - attach_workspace: + at: /tmp/ + - checkout + - run: | + VERSION=`cat /tmp/VERSION` + sed -i 's/SIDECAR_VERSION/'"$VERSION"'/g' .circleci/test/sidecar.yaml + sed -i 's/CIRCLE_PROJECT_USERNAME/'"$CIRCLE_PROJECT_USERNAME"'/g' .circleci/test/sidecar.yaml + cat .circleci/test/sidecar.yaml + - run: + name: test-in-cluster + command: .circleci/test-in-cluster.sh + no_output_timeout: 3600 + + test-k8s-1-19: + machine: true + environment: + KIND_VERSION: v0.8.1 + K8S_VERSION: v1.19.0 + steps: + - attach_workspace: + at: /tmp/ + - checkout + - run: | + VERSION=`cat /tmp/VERSION` + sed -i 's/SIDECAR_VERSION/'"$VERSION"'/g' .circleci/test/sidecar.yaml + sed -i 's/CIRCLE_PROJECT_USERNAME/'"$CIRCLE_PROJECT_USERNAME"'/g' .circleci/test/sidecar.yaml cat .circleci/test/sidecar.yaml - run: name: test-in-cluster @@ -125,46 +178,80 @@ deploy: docker: - - image: circleci/python:3.7.4 + - image: circleci/python:3.7.8 steps: - attach_workspace: at: /tmp/ - checkout - setup_remote_docker: - docker_layer_caching: true + version: 19.03.12 - run: | TAG=`cat /tmp/VERSION` + mkdir -vp ~/.docker/cli-plugins/ + curl --silent -L "https://github.com/docker/buildx/releases/download/v0.4.1/buildx-v0.4.1.linux-amd64" > ~/.docker/cli-plugins/docker-buildx + chmod a+x ~/.docker/cli-plugins/docker-buildx + docker buildx version + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes + docker context create buildcontext + docker buildx create buildcontext --use docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD - docker pull kiwigrid/k8s-sidecar-build:$TAG - docker tag kiwigrid/k8s-sidecar-build:$TAG kiwigrid/k8s-sidecar:$TAG - docker tag kiwigrid/k8s-sidecar-build:$TAG kiwigrid/k8s-sidecar:latest - docker push kiwigrid/k8s-sidecar:$TAG - docker push kiwigrid/k8s-sidecar:latest + echo "FROM ${CIRCLE_PROJECT_USERNAME}/k8s-sidecar-build:$TAG" | docker buildx build --push -t ${CIRCLE_PROJECT_USERNAME}/k8s-sidecar:$TAG -t ${CIRCLE_PROJECT_USERNAME}/k8s-sidecar:latest --platform linux/amd64,linux/arm64,linux/arm/v7 - workflows: version: 2 test_deploy: jobs: - - build - - test-k8s-1-13: - requires: - - build + - build: + filters: # required since `deploy` has tag filters AND requires `build` + tags: + only: /.*/ - test-k8s-1-14: + filters: # required since `deploy` has tag filters AND requires `build` + tags: + only: /.*/ requires: - build - test-k8s-1-15: + filters: # required since `deploy` has tag filters AND requires `build` + tags: + only: /.*/ requires: - build - test-k8s-1-16: + filters: # required since `deploy` has tag filters AND requires `build` + tags: + only: /.*/ + requires: + - build + - test-k8s-1-17: + filters: # required since `deploy` has tag filters AND requires `build` + tags: + only: /.*/ + requires: + - build + - test-k8s-1-18: + filters: # required since `deploy` has tag filters AND requires `build` + tags: + only: /.*/ + requires: + - build + - test-k8s-1-19: + filters: # required since `deploy` has tag filters AND requires `build` + tags: + only: /.*/ requires: - build - deploy: requires: - - test-k8s-1-13 - test-k8s-1-14 - test-k8s-1-15 - test-k8s-1-16 + - test-k8s-1-17 + - test-k8s-1-18 + - test-k8s-1-19 filters: branches: only: master + tags: + only: /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/ \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/.circleci/test/configmap.yaml new/k8s-sidecar-0.1.144/.circleci/test/configmap.yaml --- old/k8s-sidecar-0.1.75/.circleci/test/configmap.yaml 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/.circleci/test/configmap.yaml 2020-12-12 10:59:23.000000000 +0100 @@ -6,4 +6,13 @@ findme: "yea" data: hello.world: |- - Hello World! \ No newline at end of file + Hello World! +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: sample-configmap-binary + labels: + findme: "yea" +binaryData: + hello.binary: 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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/.circleci/test/sidecar.yaml new/k8s-sidecar-0.1.144/.circleci/test/sidecar.yaml --- old/k8s-sidecar-0.1.75/.circleci/test/sidecar.yaml 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/.circleci/test/sidecar.yaml 2020-12-12 10:59:23.000000000 +0100 @@ -34,7 +34,7 @@ serviceAccountName: sample-acc containers: - name: sidecar - image: kiwigrid/k8s-sidecar-build:SIDECAR_VERSION + image: CIRCLE_PROJECT_USERNAME/k8s-sidecar-build:SIDECAR_VERSION volumeMounts: - name: shared-volume mountPath: /tmp/ @@ -47,4 +47,4 @@ value: both volumes: - name: shared-volume - emptyDir: {} \ No newline at end of file + emptyDir: {} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/.circleci/test-in-cluster.sh new/k8s-sidecar-0.1.144/.circleci/test-in-cluster.sh --- old/k8s-sidecar-0.1.75/.circleci/test-in-cluster.sh 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/.circleci/test-in-cluster.sh 2020-12-12 10:59:23.000000000 +0100 @@ -68,7 +68,8 @@ } verify_configmap_read(){ - kubectl exec sidecar ls /tmp/hello.world + kubectl exec sidecar -- ls /tmp/hello.world + kubectl exec sidecar -- ls /tmp/hello.binary } cleanup_cluster() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/.github/workflows/main.yaml new/k8s-sidecar-0.1.144/.github/workflows/main.yaml --- old/k8s-sidecar-0.1.75/.github/workflows/main.yaml 1970-01-01 01:00:00.000000000 +0100 +++ new/k8s-sidecar-0.1.144/.github/workflows/main.yaml 2020-12-12 10:59:23.000000000 +0100 @@ -0,0 +1,18 @@ +name: Bump version +on: + push: + branches: + - master +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: '0' + - name: Bump version and push tag + uses: anothrNick/github-tag-action@1.26.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_OWNER: pulledtim + INITIAL_VERSION: 1.0.0 \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/.gitignore new/k8s-sidecar-0.1.144/.gitignore --- old/k8s-sidecar-0.1.75/.gitignore 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/.gitignore 1970-01-01 01:00:00.000000000 +0100 @@ -1,2 +0,0 @@ -*.iml -.idea/ \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/Dockerfile new/k8s-sidecar-0.1.144/Dockerfile --- old/k8s-sidecar-0.1.75/Dockerfile 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/Dockerfile 2020-12-12 10:59:23.000000000 +0100 @@ -4,11 +4,14 @@ COPY requirements.txt . RUN apk add --no-cache gcc && \ - pip install -r requirements.txt && \ - apk del gcc + pip3 install -r requirements.txt && \ + apk del -r gcc && \ + rm -rf /var/cache/apk/* requirements.txt COPY sidecar/* ./ -#run as non-privileged user -USER nobody +# Use the nobody user's numeric UID/GID to satisfy MustRunAsNonRoot PodSecurityPolicies +# https://kubernetes.io/docs/concepts/policy/pod-security-policy/#users-and-groups +USER 65534:65534 + CMD [ "python", "-u", "/app/sidecar.py" ] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/README.md new/k8s-sidecar-0.1.144/README.md --- old/k8s-sidecar-0.1.75/README.md 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/README.md 2020-12-12 10:59:23.000000000 +0100 @@ -5,7 +5,7 @@ # What? -This is a docker container intended to run inside a kubernetes cluster to collect config maps with a specified label and store the included files in an local folder. It can also send an HTTP request to a specified URL after a configmap change. The main target is to be run as a sidecar container to supply an application with information from the cluster. The contained python script is working with the Kubernetes API 1.10 +This is a docker container intended to run inside a kubernetes cluster to collect config maps with a specified label and store the included files in an local folder. It can also send an HTTP request to a specified URL after a configmap change. The main target is to be run as a sidecar container to supply an application with information from the cluster. The contained Python script is working from Kubernetes API 1.10. # Why? @@ -13,7 +13,7 @@ # How? -Run the container created by this repo together you application in an single pod with a shared volume. Specify which label should be monitored and where the files should be stored. +Run the container created by this repo together with your application in an single pod with a shared volume. Specify which label should be monitored and where the files should be stored. By adding additional env variables the container can send an HTTP request to specified URL. # Features @@ -21,13 +21,14 @@ - Extract files from config maps - Filter based on label - Update/Delete on change of configmap +- Enforce unique filenames # Usage -Example for a simple deployment can be found in `example.yaml`. Depending on the cluster setup you have to grant yourself admin rights first: `kubectl create clusterrolebinding cluster-admin-binding --clusterrole cluster-admin --user $(gcloud config get-value account)` +Example for a simple deployment can be found in [`example.yaml`](./example.yaml). Depending on the cluster setup you have to grant yourself admin rights first: `kubectl create clusterrolebinding cluster-admin-binding --clusterrole cluster-admin --user $(gcloud config get-value account)` One can override the default directory that files are copied into using a configmap annotation defined by the environment variable "FOLDER_ANNOTATION" (if not present it will default to "k8s-sidecar-target-directory"). The sidecar will attempt to create directories defined by configmaps if they are not present. Example configmap annotation: - k8s-sidecar-target-directory: "/path/to/target/directory" + `k8s-sidecar-target-directory: "/path/to/target/directory"` If the filename ends with `.url` suffix, the content will be processed as an URL the target file will be downloaded and used as the content file. @@ -38,6 +39,11 @@ - required: true - type: string +- `LABEL_VALUE` + - description: The value for the label you want to filter your resources on. Don't set a value to filter by any value + - required: false + - type: string + - `FOLDER` - description: Folder where the files should be placed - required: true @@ -60,15 +66,26 @@ - type: string - `METHOD` - - description: If `METHOD` is set with `LIST`, the sidecar will just list config-maps and exit. With `SLEEP` it will list all config-maps, then sleep for 60 seconds. Default is watch. + - description: If `METHOD` is set with `LIST`, the sidecar will just list config-maps and exit. With `SLEEP` it will list all config-maps, then sleep for `SLEEP_TIME` seconds. Default is watch. - required: false - type: string +- `SLEEP_TIME` + - description: How many seconds to wait before updating config-maps when using `SLEEP` method. + - required: false + - default: 60 + - type: integer + - `REQ_URL` - description: URL to which send a request after a configmap got reloaded - required: false - type: URI +- `REQ_USERNAME` + - description: Username to use for basic authentication + - required: false + - type: string + - `REQ_METHOD` - description: Request method GET(default) or POST - required: false @@ -79,6 +96,11 @@ - required: false - type: json +- `REQ_PASSWORD` + - description: Password to use for basic authentication + - required: false + - type: string + - `REQ_RETRY_TOTAL` - description: Total number of retries to allow - required: false @@ -104,12 +126,29 @@ - type: float - `REQ_TIMEOUT` - - description: many seconds to wait for the server to send data before giving up + - description: How many seconds to wait for the server to send data before giving up - required: false - default: 10 - type: float +- `ERROR_THROTTLE_SLEEP` + - description: How many seconds to wait before watching resources again when an error occurs + - required: false + - default: 5 + - type: integer + - `SKIP_TLS_VERIFY` - description: Set to true to skip tls verification for kube api calls - required: false - type: boolean + +- `UNIQUE_FILENAMES` + - description: Set to true to produce unique filenames where duplicate data keys exist between ConfigMaps and/or Secrets within the same or multiple Namespaces. + - required: false + - default: false + - type: boolean + +- `DEFAULT_FILE_MODE` + - description: The default file system permission for every file. Use three digits (e.g. '500', '440', ...) + - required: false + - type: string diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/requirements.txt new/k8s-sidecar-0.1.144/requirements.txt --- old/k8s-sidecar-0.1.75/requirements.txt 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/requirements.txt 2020-12-12 10:59:23.000000000 +0100 @@ -1,2 +1,2 @@ -kubernetes==10.0.0 -requests==2.22.0 +kubernetes==12.0.0 +requests==2.24.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/sidecar/helpers.py new/k8s-sidecar-0.1.144/sidecar/helpers.py --- old/k8s-sidecar-0.1.75/sidecar/helpers.py 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/sidecar/helpers.py 2020-12-12 10:59:23.000000000 +0100 @@ -1,40 +1,87 @@ +#!/usr/bin/env python + +import hashlib import os import errno import requests from requests.packages.urllib3.util.retry import Retry from requests.adapters import HTTPAdapter +from datetime import datetime -def writeTextToFile(folder, filename, data): +def writeTextToFile(folder, filename, data, data_type="ascii"): + """ + Write text to a file. If the parent folder doesn't exist, create it. If there are insufficient + permissions to create the directory, log an error and return. + """ if not os.path.exists(folder): try: os.makedirs(folder) except OSError as e: - if e.errno != errno.EEXIST: + if e.errno not in (errno.EACCES, errno.EEXIST): raise + if e.errno == errno.EACCES: + print(f"{timestamp()} Error: insufficient privileges to create {folder}. " + f"Skipping {filename}.") + return + + absolutepath = os.path.join(folder, filename) + if os.path.exists(absolutepath): + # Compare file contents with new ones so we don't update the file if nothing changed + if data_type == "binary": + sha256_hash_new = hashlib.sha256(data) + else: + sha256_hash_new = hashlib.sha256(data.encode('utf-8')) + + with open(absolutepath, 'rb') as f: + sha256_hash_cur = hashlib.sha256() + for byte_block in iter(lambda: f.read(4096),b""): + sha256_hash_cur.update(byte_block) + + if sha256_hash_new.hexdigest() == sha256_hash_cur.hexdigest(): + print(f"{timestamp()} Contents of {filename} haven't changed. Not overwriting existing file") + return False + + if data_type == "ascii": + write_type = "w" + elif data_type == "binary": + write_type = "wb" - with open(folder + "/" + filename, 'w') as f: + with open(absolutepath, write_type) as f: f.write(data) f.close() + if os.getenv('DEFAULT_FILE_MODE'): + mode = int(os.getenv('DEFAULT_FILE_MODE'), base=8) + os.chmod(absolutepath, mode) + return True def removeFile(folder, filename): - completeFile = folder + "/" + filename + completeFile = os.path.join(folder, filename) if os.path.isfile(completeFile): os.remove(completeFile) + return True else: - print(f"Error: {completeFile} file not found") + print(f"{timestamp()} Error: {completeFile} file not found") + return False def request(url, method, payload=None): - retryTotal = 5 if os.getenv('REQ_RETRY_TOTAL') is None else int(os.getenv('REQ_RETRY_TOTAL')) - retryConnect = 5 if os.getenv('REQ_RETRY_CONNECT') is None else int( - os.getenv('REQ_RETRY_CONNECT')) - retryRead = 5 if os.getenv('REQ_RETRY_READ') is None else int(os.getenv('REQ_RETRY_READ')) - retryBackoffFactor = 0.2 if os.getenv('REQ_RETRY_BACKOFF_FACTOR') is None else float( - os.getenv('REQ_RETRY_BACKOFF_FACTOR')) - timeout = 10 if os.getenv('REQ_TIMEOUT') is None else float(os.getenv('REQ_TIMEOUT')) + retryTotal = 5 if os.getenv("REQ_RETRY_TOTAL") is None else int(os.getenv("REQ_RETRY_TOTAL")) + retryConnect = 5 if os.getenv("REQ_RETRY_CONNECT") is None else int( + os.getenv("REQ_RETRY_CONNECT")) + retryRead = 5 if os.getenv("REQ_RETRY_READ") is None else int(os.getenv("REQ_RETRY_READ")) + retryBackoffFactor = 0.2 if os.getenv("REQ_RETRY_BACKOFF_FACTOR") is None else float( + os.getenv("REQ_RETRY_BACKOFF_FACTOR")) + timeout = 10 if os.getenv("REQ_TIMEOUT") is None else float(os.getenv("REQ_TIMEOUT")) + + username = os.getenv("REQ_USERNAME") + password = os.getenv("REQ_PASSWORD") + if username and password: + auth = (username, password) + else: + auth = None r = requests.Session() retries = Retry(total=retryTotal, @@ -42,16 +89,38 @@ read=retryRead, backoff_factor=retryBackoffFactor, status_forcelist=[500, 502, 503, 504]) - r.mount('http://', HTTPAdapter(max_retries=retries)) - r.mount('https://', HTTPAdapter(max_retries=retries)) + r.mount("http://", HTTPAdapter(max_retries=retries)) + r.mount("https://", HTTPAdapter(max_retries=retries)) if url is None: - print("No url provided. Doing nothing.") + print(f"{timestamp()} No url provided. Doing nothing.") return # If method is not provided use GET as default if method == "GET" or not method: - res = r.get("%s" % url, timeout=timeout) + res = r.get("%s" % url, auth=auth, timeout=timeout) elif method == "POST": - res = r.post("%s" % url, json=payload, timeout=timeout) - print(f"{method} request sent to {url}. Response: {res.status_code} {res.reason}") + res = r.post("%s" % url, auth=auth, json=payload, timeout=timeout) + print(f"{timestamp()} {method} request sent to {url}. " + f"Response: {res.status_code} {res.reason} {res.text}") return res + + +def timestamp(): + """Get a timestamp of the current time for logging.""" + return datetime.now().strftime("[%Y-%m-%d %X]") + + +def uniqueFilename(filename, namespace, resource, resource_name): + """Return a unique filename derived from the arguments provided, e.g. + "namespace_{namespace}.{configmap|secret}_{resource_name}.{filename}". + + This is used where duplicate data keys may exist between ConfigMaps + and/or Secrets within the same or multiple Namespaces. + + Keyword arguments: + filename -- the filename derived from a data key present in a ConfigMap or Secret. + namespace -- the Namespace from which data is sourced. + resource -- the resource type, e.g. "configmap" or "secret". + resource_name -- the name of the "configmap" or "secret" resource instance. + """ + return "namespace_" + namespace + "." + resource + "_" + resource_name + "." + filename diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/sidecar/resources.py new/k8s-sidecar-0.1.144/sidecar/resources.py --- old/k8s-sidecar-0.1.75/sidecar/resources.py 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/sidecar/resources.py 2020-12-12 10:59:23.000000000 +0100 @@ -1,3 +1,5 @@ +#!/usr/bin/env python + import base64 import os import sys @@ -9,8 +11,9 @@ from kubernetes import client, watch from kubernetes.client.rest import ApiException from urllib3.exceptions import ProtocolError +from urllib3.exceptions import MaxRetryError -from helpers import request, writeTextToFile, removeFile +from helpers import request, writeTextToFile, removeFile, timestamp, uniqueFilename _list_namespaced = { "secret": "list_namespaced_secret", @@ -18,7 +21,7 @@ } def signal_handler(signum, frame): - print("Subprocess exiting gracefully") + print(f"{timestamp()} Subprocess exiting gracefully") sys.exit(0) signal.signal(signal.SIGTERM, signal_handler) @@ -29,9 +32,11 @@ } -def _get_file_data_and_name(full_filename, content, resource): +def _get_file_data_and_name(full_filename, content, resource, content_type="ascii"): if resource == "secret": file_data = base64.b64decode(content).decode() + elif content_type == "binary": + file_data = base64.decodebytes(content.encode('ascii')) else: file_data = content @@ -44,137 +49,197 @@ return filename, file_data -def listResources(label, targetFolder, url, method, payload, current, folderAnnotation, resource): +def _get_destination_folder(metadata, defaultFolder, folderAnnotation): + if metadata.annotations: + if folderAnnotation in metadata.annotations.keys(): + destFolder = metadata.annotations[folderAnnotation] + print(f"{timestamp()} Found a folder override annotation, " + f"placing the {metadata.name} in: {destFolder}") + return destFolder + return defaultFolder + + +def listResources(label, labelValue, targetFolder, url, method, payload, + currentNamespace, folderAnnotation, resource, uniqueFilenames): v1 = client.CoreV1Api() - namespace = os.getenv("NAMESPACE", current) + namespace = os.getenv("NAMESPACE", currentNamespace) + # Filter resources based on label and value or just label + labelSelector=f"{label}={labelValue}" if labelValue else label + if namespace == "ALL": - ret = getattr(v1, _list_for_all_namespaces[resource])() + ret = getattr(v1, _list_for_all_namespaces[resource])(label_selector=labelSelector) else: - ret = getattr(v1, _list_namespaced[resource])(namespace=namespace) + ret = getattr(v1, _list_namespaced[resource])(namespace=namespace, label_selector=labelSelector) + + files_changed = False + # For all the found resources for sec in ret.items: - destFolder = targetFolder metadata = sec.metadata - if metadata.labels is None: - continue - print(f'Working on {resource}: {metadata.namespace}/{metadata.name}') - if label in sec.metadata.labels.keys(): - print(f"Found {resource} with label") - if sec.metadata.annotations is not None: - if folderAnnotation in sec.metadata.annotations.keys(): - destFolder = sec.metadata.annotations[folderAnnotation] - - dataMap = sec.data - if dataMap is None: - print(f"No data field in {resource}") + + print(f"{timestamp()} Working on {resource}: {metadata.namespace}/{metadata.name}") + + # Get the destination folder + destFolder = _get_destination_folder(metadata, targetFolder, folderAnnotation) + + # Check if it's an empty ConfigMap or Secret + if resource == "configmap": + if sec.data is None and sec.binary_data is None: + print(f"{timestamp()} No data/binaryData field in {resource}") continue + else: + if sec.data is None: + print(f"{timestamp()} No data field in {resource}") + continue + + # Each key on the data is a file + if sec.data is not None: + for data_key in sec.data.keys(): + filename, filedata = _get_file_data_and_name(data_key, + sec.data[data_key], + resource) + if uniqueFilenames: + filename = uniqueFilename(filename = filename, + namespace = metadata.namespace, + resource = resource, + resource_name = metadata.name) + + files_changed |= writeTextToFile(destFolder, filename, filedata) + + # Each key on the binaryData is a file + if sec.binary_data is not None: + for data_key in sec.binary_data.keys(): + filename, filedata = _get_file_data_and_name(data_key, + sec.binary_data[data_key], + resource, + content_type="binary") + if uniqueFilenames: + filename = uniqueFilename(filename = filename, + namespace = metadata.namespace, + resource = resource, + resource_name = metadata.name) - if label in sec.metadata.labels.keys(): - for data_key in dataMap.keys(): - filename, filedata = _get_file_data_and_name(data_key, dataMap[data_key], - resource) - writeTextToFile(destFolder, filename, filedata) + files_changed |= writeTextToFile(destFolder, filename, filedata, data_type="binary") - if url is not None: - request(url, method, payload) + if url and files_changed: + request(url, method, payload) -def _watch_resource_iterator(label, targetFolder, url, method, payload, - current, folderAnnotation, resource): +def _watch_resource_iterator(label, labelValue, targetFolder, url, method, payload, + currentNamespace, folderAnnotation, resource, uniqueFilenames): v1 = client.CoreV1Api() - namespace = os.getenv("NAMESPACE", current) + namespace = os.getenv("NAMESPACE", currentNamespace) + # Filter resources based on label and value or just label + labelSelector=f"{label}={labelValue}" if labelValue else label + if namespace == "ALL": - stream = watch.Watch().stream(getattr(v1, _list_for_all_namespaces[resource])) + stream = watch.Watch().stream(getattr(v1, _list_for_all_namespaces[resource]), label_selector=labelSelector) else: - stream = watch.Watch().stream(getattr(v1, _list_namespaced[resource]), namespace=namespace) + stream = watch.Watch().stream(getattr(v1, _list_namespaced[resource]), namespace=namespace, label_selector=labelSelector) + # Process events for event in stream: - destFolder = targetFolder - metadata = event['object'].metadata - if metadata.labels is None: + metadata = event["object"].metadata + + print(f"{timestamp()} Working on {resource} {metadata.namespace}/{metadata.name}") + + files_changed = False + + # Get the destination folder + destFolder = _get_destination_folder(metadata, targetFolder, folderAnnotation) + + # Check if it's an empty ConfigMap or Secret + dataMap = event["object"].data + if dataMap is None: + print(f"{timestamp()} {resource} does not have data.") continue - print(f'Working on {resource} {metadata.namespace}/{metadata.name}') - if label in event['object'].metadata.labels.keys(): - print(f"{resource} with label found") - if event['object'].metadata.annotations is not None: - if folderAnnotation in event['object'].metadata.annotations.keys(): - destFolder = event['object'].metadata.annotations[folderAnnotation] - print('Found a folder override annotation, ' - f'placing the {resource} in: {destFolder}') - dataMap = event['object'].data - if dataMap is None: - print(f"{resource} does not have data.") - continue - eventType = event['type'] - for data_key in dataMap.keys(): - print(f"File in {resource} {data_key} {eventType}") - - if (eventType == "ADDED") or (eventType == "MODIFIED"): - filename, filedata = _get_file_data_and_name(data_key, dataMap[data_key], - resource) - writeTextToFile(destFolder, filename, filedata) - - if url is not None: - request(url, method, payload) - else: - filename = data_key[:-4] if data_key.endswith(".url") else data_key - removeFile(destFolder, filename) - if url is not None: - request(url, method, payload) + + eventType = event["type"] + # Each key on the data is a file + for data_key in dataMap.keys(): + print(f"{timestamp()} File in {resource} {data_key} {eventType}") + + if (eventType == "ADDED") or (eventType == "MODIFIED"): + filename, filedata = _get_file_data_and_name(data_key, dataMap[data_key], + resource) + if uniqueFilenames: + filename = uniqueFilename(filename = filename, + namespace = metadata.namespace, + resource = resource, + resource_name = metadata.name) + + files_changed |= writeTextToFile(destFolder, filename, filedata) + else: + # Get filename from event + filename = data_key[:-4] if data_key.endswith(".url") else data_key + + if uniqueFilenames: + filename = uniqueFilename(filename = filename, + namespace = metadata.namespace, + resource = resource, + resource_name = metadata.name) + + files_changed |= removeFile(destFolder, filename) + if url and files_changed: + request(url, method, payload) def _watch_resource_loop(mode, *args): while True: try: + # Always wait to slow down the loop in case of exceptions + sleep(int(os.getenv("ERROR_THROTTLE_SLEEP", 5))) if mode == "SLEEP": listResources(*args) - sleep(60) + sleep(int(os.getenv("SLEEP_TIME", 60))) else: _watch_resource_iterator(*args) except ApiException as e: if e.status != 500: - print(f"ApiException when calling kubernetes: {e}\n") + print(f"{timestamp()} ApiException when calling kubernetes: {e}\n") else: raise except ProtocolError as e: - print(f"ProtocolError when calling kubernetes: {e}\n") + print(f"{timestamp()} ProtocolError when calling kubernetes: {e}\n") + except MaxRetryError as e: + print(f"{timestamp()} MaxRetryError when calling kubernetes: {e}\n") except Exception as e: - print(f"Received unknown exception: {e}\n") + print(f"{timestamp()} Received unknown exception: {e}\n") -def watchForChanges(mode, label, targetFolder, url, method, payload, - current, folderAnnotation, resources): +def watchForChanges(mode, label, labelValue, targetFolder, url, method, payload, + currentNamespace, folderAnnotation, resources, uniqueFilenames): firstProc = Process(target=_watch_resource_loop, - args=(mode, label, targetFolder, url, method, payload, - current, folderAnnotation, resources[0]) + args=(mode, label, labelValue, targetFolder, url, method, payload, + currentNamespace, folderAnnotation, resources[0], uniqueFilenames) ) firstProc.daemon=True firstProc.start() if len(resources) == 2: secProc = Process(target=_watch_resource_loop, - args=(mode, label, targetFolder, url, method, payload, - current, folderAnnotation, resources[1]) + args=(mode, label, labelValue, targetFolder, url, method, payload, + currentNamespace, folderAnnotation, resources[1], uniqueFilenames) ) secProc.daemon=True secProc.start() while True: if not firstProc.is_alive(): - print(f"Process for {resources[0]} died. Stopping and exiting") + print(f"{timestamp()} Process for {resources[0]} died. Stopping and exiting") if len(resources) == 2 and secProc.is_alive(): secProc.terminate() elif len(resources) == 2: - print(f"Process for {resources[1]} also died...") + print(f"{timestamp()} Process for {resources[1]} also died...") raise Exception("Loop died") if len(resources) == 2 and not secProc.is_alive(): - print(f"Process for {resources[1]} died. Stopping and exiting") + print(f"{timestamp()} Process for {resources[1]} died. Stopping and exiting") if firstProc.is_alive(): firstProc.terminate() else: - print(f"Process for {resources[0]} also died...") + print(f"{timestamp()} Process for {resources[0]} also died...") raise Exception("Loop died") sleep(5) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/k8s-sidecar-0.1.75/sidecar/sidecar.py new/k8s-sidecar-0.1.144/sidecar/sidecar.py --- old/k8s-sidecar-0.1.75/sidecar/sidecar.py 2020-02-04 11:47:23.000000000 +0100 +++ new/k8s-sidecar-0.1.144/sidecar/sidecar.py 2020-12-12 10:59:23.000000000 +0100 @@ -1,54 +1,73 @@ +#!/usr/bin/env python + import os from kubernetes import client, config from resources import listResources, watchForChanges +from helpers import timestamp def main(): - print("Starting collector") + print(f"{timestamp()} Starting collector") - folderAnnotation = os.getenv('FOLDER_ANNOTATIONS') + folderAnnotation = os.getenv("FOLDER_ANNOTATION") if folderAnnotation is None: - print("No folder annotation was provided, defaulting to k8s-sidecar-target-directory") + print(f"{timestamp()} No folder annotation was provided, " + "defaulting to k8s-sidecar-target-directory") folderAnnotation = "k8s-sidecar-target-directory" - label = os.getenv('LABEL') + label = os.getenv("LABEL") if label is None: - print("Should have added LABEL as environment variable! Exit") + print(f"{timestamp()} Should have added LABEL as environment variable! Exit") return -1 - targetFolder = os.getenv('FOLDER') + labelValue = os.getenv("LABEL_VALUE") + if labelValue: + print(f"{timestamp()} Filter labels with value: {labelValue}") + + targetFolder = os.getenv("FOLDER") if targetFolder is None: - print("Should have added FOLDER as environment variable! Exit") + print(f"{timestamp()} Should have added FOLDER as environment variable! Exit") return -1 - resources = os.getenv('RESOURCE', 'configmap') + resources = os.getenv("RESOURCE", "configmap") resources = ("secret", "configmap") if resources == "both" else (resources, ) - print(f"Selected resource type: {resources}") - - method = os.getenv('REQ_METHOD') - url = os.getenv('REQ_URL') - payload = os.getenv('REQ_PAYLOAD') + print(f"{timestamp()} Selected resource type: {resources}") - config.load_incluster_config() - print("Config for cluster api loaded...") - namespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read() + method = os.getenv("REQ_METHOD") + url = os.getenv("REQ_URL") + payload = os.getenv("REQ_PAYLOAD") + + try: + config.load_kube_config() + except: + config.load_incluster_config() + print(f"{timestamp()} Config for cluster api loaded...") + currentNamespace = open("/var/run/secrets/kubernetes.io/serviceaccount/namespace").read() - if os.getenv('SKIP_TLS_VERIFY') == 'true': + if os.getenv("SKIP_TLS_VERIFY") == "true": configuration = client.Configuration() configuration.verify_ssl = False configuration.debug = False client.Configuration.set_default(configuration) + uniqueFilenames = os.getenv("UNIQUE_FILENAMES") + if uniqueFilenames is not None and uniqueFilenames.lower() == "true": + print(f"{timestamp()} Unique filenames will be enforced.") + uniqueFilenames = True + else: + print(f"{timestamp()} Unique filenames will not be enforced.") + uniqueFilenames = False + if os.getenv("METHOD") == "LIST": for res in resources: - listResources(label, targetFolder, url, method, payload, - namespace, folderAnnotation, res) + listResources(label, labelValue, targetFolder, url, method, payload, + currentNamespace, folderAnnotation, res, uniqueFilenames) else: - watchForChanges(os.getenv("METHOD"), label, targetFolder, url, method, - payload, namespace, folderAnnotation, resources) + watchForChanges(os.getenv("METHOD"), label, labelValue, targetFolder, url, method, + payload, currentNamespace, folderAnnotation, resources, uniqueFilenames) -if __name__ == '__main__': +if __name__ == "__main__": main()