diff --git a/.github/workflows/ci-codeql-analysis.yaml b/.github/workflows/ci-codeql-analysis.yaml index 5d11890..bdebcac 100644 --- a/.github/workflows/ci-codeql-analysis.yaml +++ b/.github/workflows/ci-codeql-analysis.yaml @@ -33,14 +33,9 @@ jobs: # a pull request then we can checkout the head. fetch-depth: 2 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -51,7 +46,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -65,4 +60,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 \ No newline at end of file + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/ci-image-build.yaml b/.github/workflows/ci-image-build.yaml index d1c1941..284197e 100644 --- a/.github/workflows/ci-image-build.yaml +++ b/.github/workflows/ci-image-build.yaml @@ -22,7 +22,7 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Collect Release Tag is used to to collect information needed later in the action and expose it so it can be referenced - name: Collect Release Tag @@ -33,23 +33,31 @@ jobs: # Setup QEMU to support multi-arch builds - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - # Part of docker/build-push-action@v2; setting up the build system + # Setting up the build system - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - # Part of docker/build-push-action@v2; login to dockerhub + # Login to dockerhub - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} + # Login to ghcr.io + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_PUSH_TOKEN }} + + # Push images to DockerHub & ghcr.io - name: Build and push imageswap-init image to DockerHub if: github.repository == 'phenixblue/imageswap-webhook' timeout-minutes: 30 - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: ./app/imageswap-init/ # file should be specified relative to the repo root rather than relative to the context @@ -59,7 +67,11 @@ jobs: # push is no longer defaulted to true under v2; to push you must specify push is true push: true # Uses the releasetag output exposed by the Collect Release Tag step to set the tag under v2 - tags: thewebroot/imageswap-init:${{ steps.prep.outputs.releasetag }},thewebroot/imageswap-init:latest + tags: | + thewebroot/imageswap-init:${{ steps.prep.outputs.releasetag }} + thewebroot/imageswap-init:latest + ghcr.io/thewebroot/imageswap-init:${{ steps.prep.outputs.releasetag }} + ghcr.io/thewebroot/imageswap-init:latest # Build and push imageswap container image build-imageswap-image: @@ -69,7 +81,7 @@ jobs: steps: - name: Check out the repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Collect Release Tag is used to to collect information needed later in the action and expose it so it can be referenced - name: Collect Release Tag @@ -80,23 +92,31 @@ jobs: # Setup QEMU to support multi-arch builds - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - # Part of docker/build-push-action@v2; setting up the build system + # Setting up the build system - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - # Part of docker/build-push-action@v2; login to dockerhub + # Login to dockerhub - name: Login to DockerHub - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} + # Login to ghcr.io + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_PUSH_TOKEN }} + + # Push images to DockerHub & ghcr.io - name: Build and push imageswap image to DockerHub if: github.repository == 'phenixblue/imageswap-webhook' timeout-minutes: 30 - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: ./app/imageswap/ # file should be specified relative to the repo root rather than relative to the context @@ -106,4 +126,8 @@ jobs: # push is no longer defaulted to true under v2; to push you must specify push is true push: true # Uses the releasetag output exposed by the Collect Release Tag step to set the tag under v2 - tags: thewebroot/imageswap:${{ steps.prep.outputs.releasetag }},thewebroot/imageswap:latest + tags: | + thewebroot/imageswap:${{ steps.prep.outputs.releasetag }} + thewebroot/imageswap:latest + ghcr.io/thewebroot/imageswap:${{ steps.prep.outputs.releasetag }} + ghcr.io/thewebroot/imageswap:latest diff --git a/Makefile b/Makefile index 2191538..90e5592 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,8 @@ # NOTE: The version for both `imageswap-init` and `imageswap` should be identical for now. # Some effort will need to be put in to be able to distringuish changes to one vs. the other # in CI/Release steps. -IMAGESWAP_VERSION := v1.5.0 -IMAGESWAP_INIT_VERSION := v1.5.0 +IMAGESWAP_VERSION := v1.5.1-test1 +IMAGESWAP_INIT_VERSION := v1.5.1-test1 REPO_ROOT := $(CURDIR) APP_NAME ?= "imageswap.py" @@ -194,7 +194,7 @@ release: echo .PHONY: build-imageswap-init-latest build-imageswap-init-latest: - $(DOCKER) build -t thewebroot/imageswap-init:latest app/imageswap-init/ + $(DOCKER) build -t thewebroot/imageswap-init:latest app/imageswap-init/ --load # Push ImageSwap-Init container image to DockerHub .PHONY: push-imageswap-init-latest @@ -206,7 +206,7 @@ push-imageswap-init-latest: .PHONY: build-imageswap-latest build-imageswap-latest: - $(DOCKER) build -t thewebroot/imageswap:latest app/imageswap/ + $(DOCKER) build -t thewebroot/imageswap:latest app/imageswap/ --load # Push ImageSwap container image to DockerHub .PHONY: push-imageswap-latest @@ -222,7 +222,7 @@ build-latest: build-imageswap-init-latest push-imageswap-init-latest build-image .PHONY: build-imageswap-init-versioned build-imageswap-init-versioned: - $(DOCKER) build -t thewebroot/imageswap-init:${IMAGESWAP_INIT_VERSION} app/imageswap-init/ + $(DOCKER) build -t thewebroot/imageswap-init:${IMAGESWAP_INIT_VERSION} app/imageswap-init/ --load # Push ImageSwap-Init container image to DockerHub .PHONY: push-imageswap-init-versioned @@ -234,7 +234,7 @@ push-imageswap-init-versioned: .PHONY: build-imageswap-versioned build-imageswap-versioned: - $(DOCKER) build -t thewebroot/imageswap:${IMAGESWAP_VERSION} app/imageswap/ + $(DOCKER) build -t thewebroot/imageswap:${IMAGESWAP_VERSION} app/imageswap/ --load # Push ImageSwap container image to DockerHub .PHONY: push-imageswap-versioned diff --git a/README.md b/README.md index 07b5a5d..9289a2e 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ ImageSwap v1.4.0 has major changes ## Overview +- [Compatibility](#compatibility) - [Prereqs](#prereqs) - [Quickstart](#quickstart) - [Health Check](#health-check) @@ -61,6 +62,21 @@ ImageSwap v1.4.0 has major changes - [Contributing](./CONTRIBUTING.md) - [Adopters](./ADOPTERS.md) + +## Compatibility + +| | K8s v1.9 -> v1.18 | K8s v1.19 | K8s v1.20 | K8s v1.21 | K8s v1.22 | K8s 1.23 | K8s 1.24 | +|-------------------|-------------------|-----------|-----------|-----------|-----------|----------|----------| +| `imageswap-1.4.2` | ✓ | ✓ | +- | - | - | - | - | +| `imageswap-1.5.0` | - | ✓ | ✓ | ✓ | ✓ | +- | ??? | + +Key: + +* `✓` Tested to work as part of CI for this version of Kubernetes +* `+-` Should probably work. Tested at some point. There may be some slight mismatch between Python K8s client and server versions +* `-` Unlikely to work. Combination not tested +* `?` Status unknown. Not currently tested + ## Prereqs Kubernetes 1.19.0 or above with the `admissionregistration.k8s.io/v1` (or higher) API enabled. Verify that by the following command: @@ -126,6 +142,20 @@ ImageSwap uses a couple of images for operation - [imageswap-init](./app/imageswap-init/Dockerfile) - [imageswap](./app/imageswap/Dockerfile) +NOTE: As of v1.5.1, these images are published to DockerHub and GitHub Container Registry at the following locations: + +- docker.io/thewebroot/imageswap-init +- docker.io/thewebroot/imageswap +- ghcr.io/thewebroot/imageswap-init +- ghcr.io/thewebroot/imageswap + +The following platforms are supported: + +- linux/adm64 +- linux/arm64 + +NOTE: Certain past versions supported the `linux/ppc64le` platform, but those have been disabled for now due to an issue with Python dependencies in the Docker Buildx QEMU environment. We will work to support `linux/ppc64le` again in the future. + ### Init Container ImageSwap uses the `imageswap-init` init-container to generate/rotate a TLS cert/key pair to secure communication between the Kubernetes API and the webhook. This action takes place on Pod startup. diff --git a/app/imageswap-init/imageswap-init.py b/app/imageswap-init/imageswap-init.py index 262928a..6314539 100755 --- a/app/imageswap-init/imageswap-init.py +++ b/app/imageswap-init/imageswap-init.py @@ -31,7 +31,6 @@ import json import logging import os -import random import sys import time import yaml @@ -43,10 +42,10 @@ imageswap_tls_rootca_secret_name = "imageswap-tls-ca" imageswap_byoc_annotation = "imageswap-byoc" imageswap_service_name = "imageswap" -imageswap_tls_path = "/tls" imageswap_mwc_template_path = "/mwc" -imageswap_tls_key = "" -imageswap_tls_cert = "" +imageswap_tls_path = "/tls" +imageswap_tls_key_name = os.getenv("IMAGESWAP_TLS_KEY_NAME", "tls.cert") +imageswap_tls_cert_name = os.getenv("IMAGESWAP_TLS_CERT_NAME", "tls.key") imageswap_mwc_name = "imageswap-webhook" imageswap_mwc_template_file = f"{imageswap_mwc_template_path}/imageswap-mwc.yaml" imageswap_mwc_webhook_name = "imageswap.webhook.k8s.twr.io" @@ -369,7 +368,7 @@ def cert_expired(namespace, tls_secret): """Function to check tls certificate return number of days until expiration""" current_datetime = datetime.datetime.now() - tls_cert_decoded = base64.b64decode(tls_secret.data["cert.pem"]) + tls_cert_decoded = base64.b64decode(tls_secret.data[imageswap_tls_cert_name]) tls_cert = x509.load_pem_x509_certificate(tls_cert_decoded, default_backend()) expire_days = tls_cert.not_valid_after - current_datetime @@ -386,14 +385,11 @@ def cert_expired(namespace, tls_secret): def cert_should_update(namespace, secret_exists, tls_secret, imageswap_tls_byoc): """Function to check if tls certificate should be updated""" - tls_cert_key = "cert.pem" - tls_key_key = "key.pem" - if tls_secret.data != None: - if tls_cert_key in tls_secret.data and tls_key_key in tls_secret.data: + if imageswap_tls_cert_name in tls_secret.data and imageswap_tls_key_name in tls_secret.data: - if tls_secret.data[tls_cert_key] == "" or tls_secret.data[tls_key_key] == "": + if tls_secret.data[imageswap_tls_cert_name] == "" or tls_secret.data[imageswap_tls_key_name] == "": if imageswap_tls_byoc: @@ -460,8 +456,8 @@ def read_tls_pair(namespace, secret_name, tls_pair, core_api): return secret, tls_pair, secret_exists, False - tls_cert_pem = base64.b64decode(secret.data["cert.pem"]) - tls_key_pem = base64.b64decode(secret.data["key.pem"]) + tls_cert_pem = base64.b64decode(secret.data[imageswap_tls_cert_name]) + tls_key_pem = base64.b64decode(secret.data[imageswap_tls_key_name]) if tls_cert_pem != "" or tls_key_pem != "": @@ -532,8 +528,8 @@ def write_tls_pair( ) secret_data = { - "cert.pem": base64.b64encode(tls_pair["cert"]).decode("utf-8").rstrip(), - "key.pem": base64.b64encode(tls_pair["key"]).decode("utf-8").rstrip(), + imageswap_tls_cert_name: base64.b64encode(tls_pair["cert"]).decode("utf-8").rstrip(), + imageswap_tls_key_name: base64.b64encode(tls_pair["key"]).decode("utf-8").rstrip(), } secret = client.V1Secret(metadata=secret_metadata, data=secret_data, type="tls") @@ -571,8 +567,8 @@ def write_tls_pair( } secret.data = { - "cert.pem": base64.b64encode(tls_pair["cert"]).decode("utf-8").rstrip(), - "key.pem": base64.b64encode(tls_pair["key"]).decode("utf-8").rstrip(), + imageswap_tls_cert_name: base64.b64encode(tls_pair["cert"]).decode("utf-8").rstrip(), + imageswap_tls_key_name: base64.b64encode(tls_pair["key"]).decode("utf-8").rstrip(), } try: @@ -601,12 +597,12 @@ def write_tls_pair( # Write cert and key to files for Flask/OPA containers logging.info("Writing cert and key locally") - logging.debug(f"TLS Pair: {tls_pair}") + #logging.debug(f"TLS Pair: {tls_pair}") - with open(f"{imageswap_tls_path}/cert.pem", "wb") as cert_file: + with open(f"{imageswap_tls_path}/{imageswap_tls_cert_name}", "wb") as cert_file: cert_file.write(tls_pair["cert"]) - with open(f"{imageswap_tls_path}/key.pem", "wb") as key_file: + with open(f"{imageswap_tls_path}/{imageswap_tls_key_name}", "wb") as key_file: key_file.write(tls_pair["key"]) @@ -1120,10 +1116,6 @@ def main(): ) logging.info("ImageSwap Init") - # Wait random time to help alleviate race conditions with multiple - # replicas on startup - # wait_time = random.randint(1,10) - # time.sleep(wait_time) init_tls_pair(imageswap_namespace_name) init_mwc(imageswap_namespace_name, imageswap_tls_byoc) logging.info("Done") diff --git a/app/imageswap/config.py b/app/imageswap/config.py index 711962b..080c73c 100644 --- a/app/imageswap/config.py +++ b/app/imageswap/config.py @@ -12,9 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + +# Set Global variables +imageswap_tls_path = "/tls" +imageswap_tls_key_name = os.getenv("IMAGESWAP_TLS_KEY_NAME", "tls.cert") +imageswap_tls_cert_name = os.getenv("IMAGESWAP_TLS_CERT_NAME", "tls.key") + # Gunicorn config bind = ":5000" workers = 2 threads = 2 -certfile = "/tls/cert.pem" -keyfile = "/tls/key.pem" +certfile = f"{imageswap_tls_path}/{imageswap_tls_cert_name}" +keyfile = f"{imageswap_tls_path}/{imageswap_tls_key_name}" diff --git a/app/imageswap/imageswap.py b/app/imageswap/imageswap.py index 43b5526..b966117 100755 --- a/app/imageswap/imageswap.py +++ b/app/imageswap/imageswap.py @@ -31,6 +31,9 @@ app = Flask(__name__) # Set Global variables +imageswap_tls_path = "/tls" +imageswap_tls_key_name = os.getenv("IMAGESWAP_TLS_KEY_NAME", "tls.cert") +imageswap_tls_cert_name = os.getenv("IMAGESWAP_TLS_CERT_NAME", "tls.key") imageswap_namespace_name = os.getenv("IMAGESWAP_NAMESPACE_NAME", "imageswap-system") imageswap_pod_name = os.getenv("IMAGESWAP_POD_NAME") imageswap_disable_label = os.getenv("IMAGESWAP_DISABLE_LABEL", "k8s.twr.io/imageswap") @@ -338,7 +341,7 @@ def swap_image(container_spec): new_image = swap_maps[image_registry_key] + re.sub(r":.*/", "/", image) # If the image registry pattern is found in the original image elif image_registry_key in image: - new_image = re.sub(image_registry_key, swap_maps[image_registry_key], image) + new_image = re.sub(re.escape(image_registry_key), swap_maps[image_registry_key], image) # For everything else else: new_image = swap_maps[image_registry_key] + "/" + image @@ -362,7 +365,7 @@ def swap_image(container_spec): elif swap_maps[imageswap_maps_default_key][-1] == "-": new_image = swap_maps[imageswap_maps_default_key] + image_registry_noport + "/" + image elif image_registry_key in image: - new_image = re.sub(image_registry, swap_maps[imageswap_maps_default_key], image) + new_image = re.sub(re.escape(image_registry), swap_maps[imageswap_maps_default_key], image) else: new_image = swap_maps[imageswap_maps_default_key] + "/" + image @@ -410,7 +413,7 @@ def swap_image(container_spec): def main(): - app.logger.info("ImageSwap v1.5.0 Startup") + app.logger.info("ImageSwap v1.5.1-test1 Startup") app.run( host="0.0.0.0", @@ -418,8 +421,8 @@ def main(): debug=False, threaded=True, ssl_context=( - "./tls/cert.pem", - "./tls/key.pem", + f"{imageswap_tls_path}/{imageswap_tls_cert_name}", + f"{imageswap_tls_path}/{imageswap_tls_key_name}", ), ) diff --git a/deploy/install.yaml b/deploy/install.yaml index 4aea009..22f1c67 100644 --- a/deploy/install.yaml +++ b/deploy/install.yaml @@ -276,7 +276,7 @@ spec: runAsGroup: 1898 initContainers: - name: imageswap-init - image: thewebroot/imageswap-init:v1.5.0 + image: thewebroot/imageswap-init:v1.5.1-test1 command: [/app/imageswap-init.py] imagePullPolicy: Always securityContext: @@ -300,7 +300,7 @@ spec: mountPath: /mwc containers: - name: imageswap - image: thewebroot/imageswap:v1.5.0 + image: thewebroot/imageswap:v1.5.1-test1 ports: - containerPort: 5000 command: ["gunicorn", "imageswap:app", "--config=config.py"] diff --git a/deploy/manifests/imageswap-deploy.yaml b/deploy/manifests/imageswap-deploy.yaml index 4198149..f591cd8 100644 --- a/deploy/manifests/imageswap-deploy.yaml +++ b/deploy/manifests/imageswap-deploy.yaml @@ -22,7 +22,7 @@ spec: runAsGroup: 1898 initContainers: - name: imageswap-init - image: thewebroot/imageswap-init:v1.5.0 + image: thewebroot/imageswap-init:v1.5.1-test1 command: [/app/imageswap-init.py] imagePullPolicy: Always securityContext: @@ -46,7 +46,7 @@ spec: mountPath: /mwc containers: - name: imageswap - image: thewebroot/imageswap:v1.5.0 + image: thewebroot/imageswap:v1.5.1-test1 ports: - containerPort: 5000 command: ["gunicorn", "imageswap:app", "--config=config.py"] diff --git a/deploy/overlays/ghcr.io/kustomization.yaml b/deploy/overlays/ghcr.io/kustomization.yaml new file mode 100644 index 0000000..0cfdd01 --- /dev/null +++ b/deploy/overlays/ghcr.io/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - ../../manifests + +images: + - name: thewebroot/imageswap-init + newName: ghcr.io/thewebroot/imageswap-init + - name: thewebroot/imageswap + newName: ghcr.io/thewebroot/imageswap \ No newline at end of file diff --git a/docs/advanced_install.md b/docs/advanced_install.md index a089b0e..3e86aec 100644 --- a/docs/advanced_install.md +++ b/docs/advanced_install.md @@ -25,6 +25,7 @@ You can find some generic examples of using kustomize overlays to manage per env | DIRECTORY | DESCRIPTION | |--- |--- | | `deploy/manifests` | The base YAML manifests | +| `deploy/overlays/ghcr.io` | Base + Image references swapped to ghcr.io registry | | `deploy/overlays/development` | Development environment specific substitutions | | `deploy/overlays/production` | Production environment specific substitutions |