diff --git a/.devops.sh b/.devops.sh new file mode 100755 index 0000000..71f765a --- /dev/null +++ b/.devops.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +DEVOPS_UTILS_URL=${DEVOPS_UTILS_URL:-"gitlab.com/f5/nginx/tools/devops-utils.git"} +DEVOPS_UTILS_REF=${DEVOPS_UTILS_REF:-"master"} +rootdir=$(git rev-parse --show-toplevel) +devops_utils_dir="${rootdir}/.devops-utils" +git_update_file=${devops_utils_dir}/.last-git-update +ttl_seconds=$((60 * 60)) + +if [ "${CI}" == "true" ]; then + url="https://gitlab-ci-token:${CI_JOB_TOKEN}@${DEVOPS_UTILS_URL}" +else + # - change the first occurrence of "/" to ":" for local git clone + url="git@${DEVOPS_UTILS_URL/\//:}" +fi + +epoch=$(date +%s) + +# - get a local copy of devops-utils and update it when it's more than 1h old +if [ ! -d "$devops_utils_dir" ]; then + if ! git clone -q "${url}" "${devops_utils_dir}" --branch "${DEVOPS_UTILS_REF}" --depth 1; then + echo "ERROR: failed to clone devops-utils repo!" + exit 1 + fi + echo "$epoch" > "$git_update_file" +else + if [ $((epoch - $(cat "$git_update_file"))) -gt $ttl_seconds ]; then + cd "$devops_utils_dir" || exit + git fetch -q origin + git reset -q --hard origin/"${DEVOPS_UTILS_REF}" + echo "$epoch" > "$git_update_file" + cd .. + fi +fi + +# shellcheck disable=SC1090,SC1091 +source "${devops_utils_dir}/devops-core-services.sh" diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index f330be6..0000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,3 +0,0 @@ -# Main global owner # -##################### -* @ciroque @chrisakker diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index cc6c1d2..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' ---- -### Describe the bug - -A clear and concise description of what the bug is. - -### To reproduce - -Steps to reproduce the behavior: - -1. Deploy nginx_loadbalancer_kubernetes using -2. View output/logs/configuration on '...' -3. See error - -### Expected behavior - -A clear and concise description of what you expected to happen. - -### Your environment - -- Version of the nginx_loadbalancer_kubernetes or specific commit - -- Target deployment platform - -### Additional context - -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index d27aba8..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' ---- -### Is your feature request related to a problem? Please describe - -A clear and concise description of what the problem is. Ex. I'm always frustrated when ... - -### Describe the solution you'd like - -A clear and concise description of what you want to happen. - -### Describe alternatives you've considered - -A clear and concise description of any alternative solutions or features you've considered. - -### Additional context - -Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 4450376..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -version: 2 -updates: - - package-ecosystem: github-actions - directory: / - schedule: - interval: weekly - day: monday - time: "00:00" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index fad5aa1..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,12 +0,0 @@ -### Proposed changes - -Describe the use case and detail of the change. If this PR addresses an issue on GitHub, make sure to include a link to that issue using one of the [supported keywords](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) here in this description (not in the title of the PR). - -### Checklist - -Before creating a PR, run through this checklist and mark each as complete. - -- [ ] I have read the [`CONTRIBUTING`](https://github.com/nginxinc/nginx-loadbalancer-kubernetes/blob/main/CONTRIBUTING.md) document -- [ ] If applicable, I have added tests that prove my fix is effective or that my feature works -- [ ] If applicable, I have checked that any relevant tests pass after adding my changes -- [ ] I have updated any relevant documentation ([`README.md`](https://github.com/nginxinc/nginx-loadbalancer-kubernetes/blob/main/README.md) and [`CHANGELOG.md`](https://github.com/nginxinc/nginx-loadbalancer-kubernetes/blob/main/CHANGELOG.md)) diff --git a/.github/workflows/build-and-sign-image.yml b/.github/workflows/build-and-sign-image.yml deleted file mode 100644 index 50b087d..0000000 --- a/.github/workflows/build-and-sign-image.yml +++ /dev/null @@ -1,101 +0,0 @@ -# This workflow will build and push a signed Docker image - -name: Build and sign image - -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build_and_sign_image: - runs-on: ubuntu-latest - permissions: - contents: write - packages: write - id-token: write - security-events: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Clear Sigstore cache - run: rm -rf ~/.sigstore - - - uses: anchore/sbom-action@v0 - with: - image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - output-file: ./nginx-loadbalancer-kubernetes-${{env.GITHUB_REF_NAME}}.spdx.json - registry-username: ${{ github.actor }} - registry-password: ${{ secrets.GITHUB_TOKEN }} - - - name: Install cosign - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da #v3.7.0 - with: - cosign-release: 'v2.4.1' - - - name: Log into registry ${{ env.REGISTRY }} for ${{ github.actor }} - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@9dc751fe249ad99385a2583ee0d084c400eee04e - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build Docker Image - id: docker-build-and-push - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{github.run_number}} - - - name: Sign the published Docker images - env: - COSIGN_EXPERIMENTAL: "true" - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: cosign sign --yes "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.docker-build-and-push.outputs.digest }}" - - # NOTE: This runs statically against the latest tag in Docker Hub which was not produced by this workflow - # This should be updated once this workflow is fully implemented - - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@91713af97dc80187565512baba96e4364e983601 # 0.16.0 - continue-on-error: true - with: - image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - format: 'sarif' - output: 'trivy-results-${{ inputs.image }}.sarif' - ignore-unfixed: 'true' - - - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v2.2.11 - continue-on-error: true - with: - sarif_file: 'trivy-results-${{ inputs.image }}.sarif' - sha: ${{ github.sha }} - ref: ${{ github.ref }} - - - name: Generate Release - uses: ncipollo/release-action@v1 - with: - artifacts: | - trivy-results-${{ inputs.image }}.sarif - ./nginx-loadbalancer-kubernetes-${{env.GITHUB_REF_NAME}}.spdx.json - body: | - # Release ${{env.GITHUB_REF_NAME}} - ## Changelog - ${{ steps.meta.outputs.changelog }} - generateReleaseNotes: true - makeLatest: false - name: "${{env.GITHUB_REF_NAME}}" diff --git a/.github/workflows/run-scorecard.yml b/.github/workflows/run-scorecard.yml deleted file mode 100644 index 3bbad84..0000000 --- a/.github/workflows/run-scorecard.yml +++ /dev/null @@ -1,72 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. They are provided -# by a third-party and are governed by separate terms of service, privacy -# policy, and support documentation. - -name: Scorecard supply-chain security -on: - # For Branch-Protection check. Only the default branch is supported. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection - branch_protection_rule: - # To guarantee Maintained check is occasionally updated. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained - schedule: - - cron: '15 14 * * 3' - push: - branches: [ "main" ] - -# Declare default permissions as read only. -permissions: read-all - -jobs: - analysis: - name: Scorecard analysis - runs-on: ubuntu-latest - permissions: - # Needed to upload the results to code-scanning dashboard. - security-events: write - # Needed to publish results and get a badge (see publish_results below). - id-token: write - # Uncomment the permissions below if installing in a private repository. - # contents: read - # actions: read - - steps: - - name: "Checkout code" - uses: actions/checkout@v4 # v3.1.0 - with: - persist-credentials: false - - - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 - with: - results_file: results.sarif - results_format: sarif - # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: - # - you want to enable the Branch-Protection check on a *public* repository, or - # - you are installing Scorecard on a *private* repository - # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. - # repo_token: ${{ secrets.SCORECARD_TOKEN }} - - # Public repositories: - # - Publish results to OpenSSF REST API for easy access by consumers - # - Allows the repository to include the Scorecard badge. - # - See https://github.com/ossf/scorecard-action#publishing-results. - # For private repositories: - # - `publish_results` will always be set to `false`, regardless - # of the value entered here. - publish_results: true - - # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF - # format to the repository Actions tab. - - name: "Upload artifact" - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - # Upload the results to GitHub's code scanning dashboard. - - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 - with: - sarif_file: results.sarif diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index 8d3d069..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,27 +0,0 @@ -# This workflow will build a golang project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go - -name: Run tests - -on: - branch_protection_rule: - types: - - created - -jobs: - - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.19 - - - name: Build - run: go build -v ./... - - - name: Test - run: go test -v ./... diff --git a/.gitignore b/.gitignore index cb5c33a..e69f538 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,69 @@ cover* tmp/ docs/tls/DESIGN.md :q -qqq \ No newline at end of file +qqq.env +.env* +!.env.example +!.allowed_clients.json +!.env.example.auth +*.db +priv/certs +priv/nginx-agent/* +!priv/nginx-agent/nginx-agent.conf.example +key-data.json +nginx-instance-manager.tar.gz +vendor/ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Output from debugger +__debug_bin + +code-quality.json +coverage/* + +# vim +*~ +*.swp + +### VisualStudioCode (from https://gitignore.io/api/VisualStudioCode) ### +.vscode/* +!.vscode/tasks.example.json +!.vscode/launch.example.json +!.vscode/extensions.json +!.vscode/KubernetesLocalProcessConfig*.yaml +*.code-workspace + +### Goland +.idea/* + +# bridge to kubernetes artifact +/KubernetesLocalProcessConfig.yaml + + +# output directory for build artifacts +build + +# output directory for test artifacts (eg. coverage report, junit xml) +results + +# devops-utils repo +.devops-utils/ + +# Ignore golang cache in CI +.go/pkg/mod + +.go-build + +nginxaas-loadbalancer-kubernetes-* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..41c7290 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,255 @@ +--- +include: + - project: "f5/nginx/tools/devops-utils" + file: "/include/cxsast-ci-generic.yml" + ref: "master" + - project: "f5/nginx/tools/devops-utils" + file: "/include/devops-docker-cicd.yaml" + ref: "master" + - project: "f5/nginx/tools/devops-utils" + file: "include/devops-whitesource.yaml" + ref: "master" + - template: Jobs/SAST.gitlab-ci.yml + - template: Jobs/Container-Scanning.gitlab-ci.yml + +stages: + - lint+test+build + - security scanning + - e2e-tests + - release + - release-cnab + +variables: + DEVTOOLS_IMG: ${DEVOPS_DOCKER_URL_DEFAULT}/nginx-azure-lb/nlb-devtools:latest + CNAB_IMG: mcr.microsoft.com/container-package-app:latest + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS + when: never + - if: '$CI_COMMIT_BRANCH || $CI_COMMIT_TAG' + +.import-devops-core-services: &import-devops-core-services | + source ${CI_PROJECT_DIR}/.devops.sh + +.go-cache: + variables: + GOPATH: $CI_PROJECT_DIR/.go + cache: + key: + files: + - go.mod + - go.sum + paths: + - .go/pkg/mod/ + +.go-cache-readonly: + extends: + - .go-cache + cache: + policy: pull + +.golang-private: &golang-private + - | + cat << EOF > ~/.netrc + machine gitlab.com + login gitlab-ci-token + password ${CI_JOB_TOKEN} + EOF + go env -w GOPRIVATE="gitlab.com/f5" + go env + +.unit-test-common: + stage: lint+test+build + image: $DEVTOOLS_IMG + extends: + - .default-runner-large + - .go-cache-readonly + script: + - *golang-private + - time make test + coverage: '/^total:\s+\(statements\)\s+(\d+\.\d+\%)$/' + artifacts: + when: + always + paths: + - results + expire_in: 1 day + reports: + junit: results/report.xml + +lint + unit-test + build: + stage: lint+test+build + image: $DEVTOOLS_IMG + extends: + - .devops-docker-cicd-large + - .go-cache + script: + - *golang-private + - | + if [ "$CI_COMMIT_BRANCH" != "$CI_DEFAULT_BRANCH" ]; then + time make helm-lint + time make lint + git diff --exit-code + time make test + fi + time make publish + time make publish-helm + coverage: '/^total:\s+\(statements\)\s+(\d+\.\d+\%)$/' + artifacts: + when: + always + paths: + - results + expire_in: 3 hours + reports: + junit: results/report.xml + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: never + - if: '$CI_COMMIT_BRANCH || $CI_MERGE_REQUEST_ID' + +validate-cnab: + stage: lint+test+build + image: $CNAB_IMG + extends: + - .devops-docker-cicd-large + script: + - tdnf install -y make + - curl -L https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq + - make cnab-validate + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: never + - if: '$CI_COMMIT_BRANCH || $CI_MERGE_REQUEST_ID' + +unit-test-data-race: + variables: + GO_DATA_RACE: "true" + extends: + - .unit-test-common + rules: + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule" && $GO_DATA_RACE == "true"' + +coverage: + extends: + - .unit-test-common + rules: + - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_SOURCE == "schedule"' + +whitesource-scan: + stage: lint+test+build + extends: + - .default-runner + - .go-cache + - .whitesource-template-go + variables: + PRODUCT_PREFIX: "n4a" + WS_PROJECT: "${CI_PROJECT_NAME}" + script: + - *golang-private + - !reference [.whitesource-template-go, script] + rules: + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: never + - !reference [.whitesource-template-go, rules] + artifacts: + when: always + paths: + - ${CI_PROJECT_DIR}/whitesource/ + expire_in: 1 weeks + +sast: + stage: lint+test+build + +# ===================================================================================== +# CISO-required, SAST-scanning jobs involving the widely distributed checkmarx vendor +# Job starts immediately and is independent of all other jobs present in the pipeline. +# ===================================================================================== +checkmarx-scan: + stage: lint+test+build + extends: + # Please DO NOT overwrite extended job's image, gitlab-runner tags, or job rules + - .checkmarx-scan-security + variables: + # project specific variables + CX_SOURCES: "." + CX_FLOW_ZIP_EXCLUDE: "" + needs: [] + +# https://docs.gitlab.com/ee/user/application_security/container_scanning/ +container_scanning: + stage: security scanning + extends: + - .devops-docker-cicd + variables: + CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN: "false" + GIT_STRATEGY: "fetch" + # CI_COMMIT_SHORT_SHA tag is being used to uniquely scan images being produced (or reproduced) on a feature branch. + CS_IMAGE: "${DEVOPS_DOCKER_URL_DEFAULT}/nginx-azure-lb/${CI_PROJECT_NAME}/nginxaas-loadbalancer-kubernetes:${CI_COMMIT_SHORT_SHA}" + # Gitlab container scanner needs the tenant ID to be set as it is using Azure auth libs underneath. + AZURE_TENANT_ID: ${ARM_TENANT_ID} + before_script: + # The Gitlab container scan image runs as a non-root user, which leads to the DevOps abstraction failing to bootstrap + # as the CI directories on the runner as seeded as "root". + - git config --global --add safe.directory "${CI_PROJECT_DIR}" + # Prerequisite toolchain to run the DevOps abstraction. + - sudo apt-get update && sudo apt-get install -y curl jq + - *import-devops-core-services + - devops.backend.docker.authenticate + - export CS_REGISTRY_USER="${DEVOPS_DOCKER_USER}" CS_REGISTRY_PASSWORD="${DEVOPS_DOCKER_PASS}" + rules: + - if: '$CI_COMMIT_TAG =~ /^v[\d]+\.[\d]+\.[\d]+/' + when: never + - if: '$CI_PIPELINE_SOURCE == "schedule"' + when: never + - when: on_success + +trigger-e2e: + stage: e2e-tests + variables: + IS_TEST_ONLY: "true" + TEST_TYPE: "e2e.arm" + TEST_NLK_CHART_URL: "oci://${DEVOPS_DOCKER_URL_DEFAULT}/nginx-azure-lb/${CI_PROJECT_NAME}/charts/${CI_COMMIT_REF_SLUG}/nginxaas-loadbalancer-kubernetes" + TEST_NLK_IMG_TAG: ${CI_COMMIT_SHORT_SHA} + TEST_ARGS: "test_nlk.py" + trigger: + project: f5/nginx/nginxazurelb/tools/nlbtest + branch: main + strategy: depend + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + +.release-common: + stage: release + image: $DEVTOOLS_IMG + extends: + - .devops-docker-cicd + rules: + - if: '$CI_COMMIT_TAG =~ /^v[\d]+\.[\d]+\.[\d]+/' + +dockerhub-image-release: + extends: + - .devops-docker-cicd + - .release-common + script: + - ./scripts/release.sh docker-image + +dockerhub-helm-release: + extends: + - .devops-docker-cicd + - .release-common + script: + - ./scripts/release.sh helm-chart + +cnab-release: + extends: + - .devops-docker-cicd + - .release-common + stage: release-cnab + image: $CNAB_IMG + script: + - tdnf install -y make wget + - curl -L https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq + - make cnab-package diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..268e39f --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,92 @@ +# GolangCI-Lint settings + +# Disable all linters and enable the required ones +linters: + disable-all: true + + # Supported linters: https://golangci-lint.run/usage/linters/ + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + - bodyclose + - dupl + - gochecknoinits + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - gosec + - lll + - misspell + - nakedret + - prealloc + - stylecheck + - unconvert + - unparam + - paralleltest + - forbidigo + fast: false + +# Run options +run: + # 10 minute timeout for analysis + timeout: 10m +# Specific linter settings +linters-settings: + gocyclo: + # Minimal code complexity to report + min-complexity: 16 + govet: + disable-all: true + enable: + # Report shadowed variables + - shadow + + misspell: + # Correct spellings using locale preferences for US + locale: US + goimports: + # Put imports beginning with prefix after 3rd-party packages + local-prefixes: gitswarm.f5net.com/indigo,gitlab.com/f5 + exhaustruct: + # List of regular expressions to match struct packages and names. + # If this list is empty, all structs are tested. + # Default: [] + include: + - "gitlab.com/f5/nginx/nginxazurelb/azure-resource-provider/pkg/token.TokenID" + - "gitlab.com/f5/nginx/nginxazurelb/azure-resource-provider/internal/dpo/agent/certificates.CertGetRequest" + +issues: + exclude-dirs: + - .go/pkg/mod + # Exclude configuration + exclude-rules: + # Exclude gochecknoinits and gosec from running on tests files + - path: _test\.go + linters: + - gochecknoinits + - gosec + - path: test/* + linters: + - gochecknoinits + - gosec + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " + # Exclude false positive paralleltest error : Range statement for test case does not use range value in test Run + - linters: + - paralleltest + text: "does not use range value in test Run" + + # Disable maximum issues count per one linter + max-issues-per-linter: 0 + + # Disable maximum count of issues with the same text + max-same-issues: 0 diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index e0217ba..0000000 --- a/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -golang 1.23.3 diff --git a/Dockerfile b/Dockerfile index 9ec8bcc..e53aef4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,11 @@ -# Copyright 2023 f5 Inc. All rights reserved. -# Use of this source code is governed by the Apache -# license that can be found in the LICENSE file. +FROM alpine:3.14.1 AS base-certs +RUN apk update && apk add --no-cache ca-certificates -FROM golang:1.23.3-alpine3.20 AS builder +FROM scratch AS base +COPY docker-user /etc/passwd +USER 101 +COPY --from=base-certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -WORKDIR /app - -COPY go.mod go.sum ./ - -RUN go mod download - -COPY . . - -RUN go build -o nginx-loadbalancer-kubernetes ./cmd/nginx-loadbalancer-kubernetes/main.go - -FROM alpine:3.20 - -WORKDIR /opt/nginx-loadbalancer-kubernetes - -RUN adduser -u 11115 -D -H nlk - -USER nlk - -COPY --from=builder /app/nginx-loadbalancer-kubernetes . - -ENTRYPOINT ["/opt/nginx-loadbalancer-kubernetes/nginx-loadbalancer-kubernetes"] +FROM base as nginxaas-loadbalancer-kubernetes +ENTRYPOINT ["/nginxaas-loadbalancer-kubernetes"] +COPY build/nginxaas-loadbalancer-kubernetes / diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c45e2cf --- /dev/null +++ b/Makefile @@ -0,0 +1,76 @@ +# - init general vars +BUILD_DIR = build +export BUILD_DIR +RESULTS_DIR = results +export RESULTS_DIR +VERSION = $(shell cat version) +export VERSION + +DOCKER_REGISTRY ?= local +DOCKER_TAG ?= latest + +# - init go vars +GOPRIVATE = *.f5net.com,gitlab.com/f5 +export GOPRIVATE + +.PHONY: default tools deps fmt lint test build build.docker publish helm-lint deploy + +default: build + +tools: + @go install gotest.tools/gotestsum@latest + @go install golang.org/x/tools/cmd/goimports@latest + @go install github.com/jstemmer/go-junit-report@v1.0.0 + @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin v1.64.5 + +deps: + @go mod download + @go mod tidy + @go mod verify + +fmt: + @find . -type f -name "*.go" -exec goimports -e -w {} \+ + +lint: + @find . -type f -not -path "./.go/pkg/mod/*" -name "*.go" -exec goimports -e -w {} \+ + @ git diff --exit-code + @golangci-lint run -v ./... + +helm-lint: + helm lint charts/nlk/ + +test: + @./scripts/test.sh + +build: + @./scripts/build.sh + +build-linux: + @./scripts/build.sh linux + +build-linux-docker: + @./scripts/docker.sh build + +publish-helm: + @scripts/docker-login.sh + @scripts/publish-helm.sh + +publish: build-linux build-linux-docker + @scripts/docker-login.sh + @./scripts/docker.sh publish + +deploy: + @[ -f $(KUBECONFIG) ] || (echo "KUBECONFIG not found." && false) + $(MAKE) publish + @scripts/deploy.sh + +clean: + rm -rf $(BUILD_DIR)/ + +.PHONY: cnab-validate cnab-package + +cnab-validate: + @./scripts/cnab.sh validate + +cnab-package: + @./scripts/cnab.sh package diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..7b18e6e --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,50 @@ +# Releasing NLK + +## Releasing a version to our internal dockerhub registry + +- Go to the [NLK repo](https://gitlab.com/f5/nginx/nginxazurelb/nginxaas-loadbalancer-kubernetes). + +- [Create a new tag](https://gitlab.com/f5/nginx/nginxazurelb/nginxaas-loadbalancer-kubernetes/-/tags/new). + +- Give the tag the name of the version to be released, e.g. "v1.1.1". This should match the version in the `version` file at the root of the repo. + +- Under **Create from** select the branch from which the image will be created. + +- Hit **Create tag**. + +## Releasing a version to Azure Marketplace + +This workflow requires Azure Marketplace permissions which few members of NGINXaaS possess (currently Ken, Ashok and Ryan). + +- Complete the steps above to publish the image internally. + +- Go to "Marketplace Offers" + +- Click on "F5 NGINX LoadBalancer for Kubernetes" + +- Under "Plan overview" select the plan which has a "Live" status (this should be "F5 NGINXaaS AKS Extension") + +- On a panel on the left hand side, select "Technical Configuration" + +- A pop up appears. + - Under "Registry" select the "nlbmarketplaceacrprod" option + - Under "Repo" select "marketplace/nginxaas-loadbalancer-kubernetes" + - Under "CNAB Bundle" select the version you wish to publish + +- To complete the publishing of the image click "Add CNAB Button" button on the bottom of the popup. + +- Select "Save draft". + +- This should take you to a "Review and Publish" screen. If the UI seems to stall. Follow steps below. + + - Next to "Marketplace offers" on the top of the screen, select "F5 NGINX Loadbalancer for Kubernetes". + + - Select "Offer overview" from the panel on the left. + + - Next to the heading "F5 NGINX Loadbalancer for Kubernetes | Offer Overview" select "Review and publish" + +- A number of items should appear, but they must include "F5 NGINXaaS AKS extenstion". Leave all items as they are. + +- Then click "Publish". + +- This will take a while. Check in on it after 24 hours. diff --git a/charts/armTemplate.json b/charts/armTemplate.json new file mode 100644 index 0000000..06c3703 --- /dev/null +++ b/charts/armTemplate.json @@ -0,0 +1,256 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "extensionResourceName": { + "type": "string", + "metadata": { + "description": "The name of the extension." + } + }, + "extensionNamespace": { + "type": "string", + "defaultValue": "nlk" + }, + "clusterResourceName": { + "type": "String", + "metadata": { + "description": "The name of the Managed Cluster resource." + } + }, + "createNewCluster": { + "type": "Bool", + "defaultValue": true, + "metadata": { + "description": "When set to 'true', creates new AKS cluster. Otherwise, an existing cluster is used." + } + }, + "location": { + "type": "String", + "metadata": { + "description": "The location of AKS resource." + } + }, + "extensionAutoUpgrade": { + "defaultValue": false, + "metadata": { + "description": "Allow auto upgrade of minor version for the extension." + }, + "type": "bool" + }, + "nginxaasDataplaneApiKey": { + "type": "String" + }, + "nginxaasDataplaneApiEndpoint": { + "type": "String" + }, + "vmSize": { + "type": "String", + "defaultValue": "Standard_DS2_v2", + "metadata": { + "description": "VM size" + } + }, + "vmEnableAutoScale": { + "type": "Bool", + "defaultValue": true, + "metadata": { + "description": "enable auto scaling" + } + }, + "vmCount": { + "type": "Int", + "defaultValue": 3, + "metadata": { + "description": "VM count" + } + }, + "dnsPrefix": { + "defaultValue": "[concat(parameters('clusterResourceName'),'-dns')]", + "type": "String", + "metadata": { + "description": "Optional DNS prefix to use with hosted Kubernetes API server FQDN." + } + }, + "osDiskSizeGB": { + "defaultValue": 0, + "minValue": 0, + "maxValue": 1023, + "type": "Int", + "metadata": { + "description": "Disk size (in GiB) to provision for each of the agent pool nodes. This value ranges from 0 to 1023. Specifying 0 will apply the default disk size for that agentVMSize." + } + }, + "kubernetesVersion": { + "type": "String", + "defaultValue": "1.26.3", + "metadata": { + "description": "The version of Kubernetes." + } + }, + "networkPlugin": { + "defaultValue": "kubenet", + "allowedValues": [ + "azure", + "kubenet" + ], + "type": "String", + "metadata": { + "description": "Network plugin used for building Kubernetes network." + } + }, + "enableRBAC": { + "defaultValue": true, + "type": "Bool", + "metadata": { + "description": "Boolean flag to turn on and off of RBAC." + } + }, + "enablePrivateCluster": { + "defaultValue": false, + "type": "Bool", + "metadata": { + "description": "Enable private network access to the Kubernetes cluster." + } + }, + "enableHttpApplicationRouting": { + "defaultValue": true, + "type": "Bool", + "metadata": { + "description": "Boolean flag to turn on and off http application routing." + } + }, + "enableAzurePolicy": { + "defaultValue": false, + "type": "Bool", + "metadata": { + "description": "Boolean flag to turn on and off Azure Policy addon." + } + }, + "enableSecretStoreCSIDriver": { + "defaultValue": false, + "type": "Bool", + "metadata": { + "description": "Boolean flag to turn on and off secret store CSI driver." + } + }, + "osSKU": { + "type": "string", + "defaultValue": "AzureLinux", + "allowedValues": [ + "AzureLinux", + "Ubuntu" + ], + "metadata": { + "description": "The Linux SKU to use." + } + }, + "enableFIPS": { + "type": "Bool", + "defaultValue": true, + "metadata": { + "description": "Enable FIPS. https://learn.microsoft.com/en-us/azure/aks/create-node-pools#fips-enabled-node-pools" + } + } + }, + "variables": { + "plan-name": "DONOTMODIFY", + "plan-publisher": "DONOTMODIFY", + "plan-offerID": "DONOTMODIFY", + "releaseTrain": "DONOTMODIFY", + "clusterExtensionTypeName": "DONOTMODIFY" + }, + "resources": [ + { + "type": "Microsoft.ContainerService/managedClusters", + "condition": "[parameters('createNewCluster')]", + "apiVersion": "2023-11-01", + "name": "[parameters('clusterResourceName')]", + "location": "[parameters('location')]", + "dependsOn": [], + "tags": {}, + "sku": { + "name": "Basic", + "tier": "Free" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "kubernetesVersion": "[parameters('kubernetesVersion')]", + "enableRBAC": "[parameters('enableRBAC')]", + "dnsPrefix": "[parameters('dnsPrefix')]", + "agentPoolProfiles": [ + { + "name": "agentpool", + "osDiskSizeGB": "[parameters('osDiskSizeGB')]", + "count": "[parameters('vmCount')]", + "enableAutoScaling": "[parameters('vmEnableAutoScale')]", + "enableFIPS": "[parameters('enableFIPS')]", + "minCount": "[if(parameters('vmEnableAutoScale'), 1, json('null'))]", + "maxCount": "[if(parameters('vmEnableAutoScale'), 10, json('null'))]", + "vmSize": "[parameters('vmSize')]", + "osType": "Linux", + "osSKU": "[parameters('osSKU')]", + "storageProfile": "ManagedDisks", + "type": "VirtualMachineScaleSets", + "mode": "System", + "maxPods": 110, + "availabilityZones": [], + "enableNodePublicIP": false, + "tags": {} + } + ], + "networkProfile": { + "loadBalancerSku": "standard", + "networkPlugin": "[parameters('networkPlugin')]" + }, + "apiServerAccessProfile": { + "enablePrivateCluster": "[parameters('enablePrivateCluster')]" + }, + "addonProfiles": { + "httpApplicationRouting": { + "enabled": "[parameters('enableHttpApplicationRouting')]" + }, + "azurepolicy": { + "enabled": "[parameters('enableAzurePolicy')]" + }, + "azureKeyvaultSecretsProvider": { + "enabled": "[parameters('enableSecretStoreCSIDriver')]" + } + } + } + }, + { + "type": "Microsoft.KubernetesConfiguration/extensions", + "apiVersion": "2023-05-01", + "name": "[parameters('extensionResourceName')]", + "properties": { + "extensionType": "[variables('clusterExtensionTypeName')]", + "autoUpgradeMinorVersion": "[parameters('extensionAutoUpgrade')]", + "releaseTrain": "[variables('releaseTrain')]", + "configurationSettings": { + "nlk.dataplaneApiKey": "[parameters('nginxaasDataplaneApiKey')]", + "nlk.config.nginxHosts": "[parameters('nginxaasDataplaneApiEndpoint')]" + }, + "configurationProtectedSettings": {}, + "scope": { + "cluster": { + "releaseNamespace": "[parameters('extensionNamespace')]" + } + } + }, + "plan": { + "name": "[variables('plan-name')]", + "publisher": "[variables('plan-publisher')]", + "product": "[variables('plan-offerID')]" + }, + "scope": "[concat('Microsoft.ContainerService/managedClusters/', parameters('clusterResourceName'))]", + "dependsOn": [ + "[resourceId('Microsoft.ContainerService/managedClusters/', parameters('clusterResourceName'))]" + ] + } + ], + "outputs": { + } +} diff --git a/charts/createUIDefinition.json b/charts/createUIDefinition.json new file mode 100644 index 0000000..c7566d2 --- /dev/null +++ b/charts/createUIDefinition.json @@ -0,0 +1,264 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "isWizard": false, + "basics": { + "location": { + "visible": "[basics('createNewCluster')]", + "resourceTypes": ["Nginx.NginxPlus/nginxDeployments"] + }, + "resourceGroup": { + "allowExisting": true + } + } + }, + "basics": [ + { + "name": "createNewCluster", + "type": "Microsoft.Common.OptionsGroup", + "label": "Create new AKS cluster", + "defaultValue": "No", + "toolTip": "Create a new AKS cluster to install the extension.", + "constraints": { + "allowedValues": [ + { + "label": "Yes", + "value": true + }, + { + "label": "No", + "value": false + } + ], + "required": true + }, + "visible": true + } + ], + "steps": [ + { + "name": "clusterDetails", + "label": "Cluster Details", + "elements": [ + { + "name": "existingClusterSection", + "type": "Microsoft.Common.Section", + "elements": [ + { + "name": "clusterLookupControl", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(subscription().id, '/resourcegroups/', resourceGroup().name, '/providers/Microsoft.ContainerService/managedClusters?api-version=2022-03-01')]" + } + }, + { + "name": "existingClusterResourceName", + "type": "Microsoft.Common.DropDown", + "label": "AKS Cluster Name", + "toolTip": "The resource name of the existing AKS cluster.", + "constraints": { + "allowedValues": "[map(steps('clusterDetails').existingClusterSection.clusterLookupControl.value, (item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.name, '\"}')))]", + "required": true + } + } + ], + "visible": "[equals(basics('createNewCluster'), false)]" + }, + { + "name": "newClusterSection", + "type": "Microsoft.Common.Section", + "elements": [ + { + "name": "aksVersionLookupControl", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(subscription().id, '/providers/Microsoft.ContainerService/locations/', location(), '/orchestrators?api-version=2019-04-01&resource-type=managedClusters')]" + } + }, + { + "name": "newClusterResourceName", + "type": "Microsoft.Common.TextBox", + "label": "AKS cluster name", + "defaultValue": "", + "toolTip": "The resource name of the new AKS cluster. Use only allowed characters", + "constraints": { + "required": true, + "regex": "^[a-z0-9A-Z]{6,30}$", + "validationMessage": "Only alphanumeric characters are allowed, and the value must be 6-30 characters long." + } + }, + { + "name": "kubernetesVersion", + "type": "Microsoft.Common.DropDown", + "label": "Kubernetes version", + "toolTip": "The version of Kubernetes that should be used for this cluster. You will be able to upgrade this version after creating the cluster.", + "constraints": { + "allowedValues": "[map(steps('clusterDetails').newClusterSection.aksVersionLookupControl.properties.orchestrators, (item) => parse(concat('{\"label\":\"', item.orchestratorVersion, '\",\"value\":\"', item.orchestratorVersion, '\"}')))]", + "required": true + } + }, + { + "name": "vmSize", + "type": "Microsoft.Compute.SizeSelector", + "label": "VM size", + "toolTip": "The size of virtual machine of AKS worker nodes.", + "recommendedSizes": [ + "Standard_B4ms", + "Standard_DS2_v2", + "Standard_D4s_v3" + ], + "constraints": { + "allowedSizes": [ + "Standard_B4ms", + "Standard_DS2_v2", + "Standard_D4s_v3" + ], + "excludedSizes": [] + }, + "osPlatform": "Linux" + }, + { + "name": "osSKU", + "type": "Microsoft.Common.DropDown", + "label": "OS SKU", + "toolTip": "The SKU of Linux OS for VM.", + "defaultValue": "Ubuntu", + "constraints": { + "allowedValues": [ + { + "label": "Ubuntu", + "value": "Ubuntu" + }, + { + "label": "AzureLinux", + "value": "AzureLinux" + } + ], + "required": true + } + }, + { + "name": "enableAutoScaling", + "type": "Microsoft.Common.CheckBox", + "label": "Enable auto scaling", + "toolTip": "Enable auto scaling", + "defaultValue": true + }, + { + "name": "vmCount", + "type": "Microsoft.Common.Slider", + "min": 1, + "max": 10, + "label": "Number of AKS worker nodes", + "subLabel": "", + "defaultValue": 1, + "showStepMarkers": false, + "toolTip": "Specify the number of AKS worker nodes.", + "constraints": { + "required": false + }, + "visible": true + } + ], + "visible": "[basics('createNewCluster')]" + } + ] + }, + { + "name": "applicationDetails", + "label": "Application Details", + "elements": [ + { + "name": "extensionResourceName", + "type": "Microsoft.Common.TextBox", + "label": "Cluster extension resource name", + "defaultValue": "", + "toolTip": "Only lowercase alphanumeric characters are allowed, and the value must be 6-30 characters long.", + "constraints": { + "required": true, + "regex": "^[a-z0-9]{6,30}$", + "validationMessage": "Only lowercase alphanumeric characters are allowed, and the value must be 6-30 characters long." + }, + "visible": true + }, + { + "name": "extensionNamespace", + "type": "Microsoft.Common.TextBox", + "label": "Installation namespace", + "defaultValue": "nlk", + "toolTip": "Only lowercase alphanumeric characters are allowed, and the value must be 6-30 characters long.", + "constraints": { + "required": true, + "regex": "^[a-z0-9]{3,30}$", + "validationMessage": "Only lowercase alphanumeric characters are allowed, and the value must be 6-30 characters long." + }, + "visible": true + }, + { + "name": "extensionAutoUpgrade", + "type": "Microsoft.Common.CheckBox", + "label": "Allow minor version upgrades of extension", + "toolTip": "Allow exntension to be upgraded automatically to latest minor version.", + "visible": true + }, + { + "name": "nginxaasDataplaneApiKey", + "type": "Microsoft.Common.TextBox", + "label": "NGINXaaS Dataplane API Key", + "defaultValue": "", + "toolTip": "The Dataplane API Key for your NGINXaaS for Azure deployment.", + "constraints": { + "required": false, + "regex": ".*", + "validationMessage": "Use the dataplane API key for your deployment." + }, + "visible": true + }, + { + "name": "nginxaasDataplaneApiEndpoint", + "type": "Microsoft.Common.TextBox", + "label": "NGINXaaS Dataplane API Endpoint", + "defaultValue": "", + "toolTip": "The Dataplane API Endpoint for your NGINXaaS for Azure deployment.", + "constraints": { + "required": false, + "regex": ".*", + "validationMessage": "Retreive the dataplane API endpoint from your deployment." + }, + "visible": true + }, + { + "name": "additionalProductInfo", + "type": "Microsoft.Common.InfoBox", + "visible": true, + "options": { + "icon": "Info", + "text": "Learn more about NGINXaaS for Azure.", + "uri": "https://docs.nginx.com/nginxaas/azure/" + } + } + ] + } + ], + "outputs": { + "location": "[location()]", + "createNewCluster": "[basics('createNewCluster')]", + "clusterResourceName": "[if(basics('createNewCluster'), steps('clusterDetails').newClusterSection.newClusterResourceName, steps('clusterDetails').existingClusterSection.existingClusterResourceName)]", + "kubernetesVersion": "[steps('clusterDetails').newClusterSection.kubernetesVersion]", + "vmSize": "[steps('clusterDetails').newClusterSection.vmSize]", + "osSKU": "[steps('clusterDetails').newClusterSection.osSKU]", + "vmEnableAutoScale": "[steps('clusterDetails').newClusterSection.enableAutoScaling]", + "vmCount": "[steps('clusterDetails').newClusterSection.vmCount]", + "extensionResourceName": "[steps('applicationDetails').extensionResourceName]", + "extensionAutoUpgrade": "[steps('applicationDetails').extensionAutoUpgrade]", + "extensionNamespace": "[steps('applicationDetails').extensionNamespace]", + "nginxaasDataplaneApiKey": "[steps('applicationDetails').nginxaasDataplaneApiKey]", + "nginxaasDataplaneApiEndpoint": "[steps('applicationDetails').nginxaasDataplaneApiEndpoint]" + } + } +} diff --git a/charts/manifest.yaml b/charts/manifest.yaml new file mode 100644 index 0000000..6883ea9 --- /dev/null +++ b/charts/manifest.yaml @@ -0,0 +1,11 @@ +applicationName: "marketplace/nginxaas-loadbalancer-kubernetes" +publisher: "F5, Inc." +description: "A component that manages NGINXaaS for Azure deployment and makes it act as Load Balancer for kubernetes workloads." +version: 0.4.0 +helmChart: "./nlk" +clusterArmTemplate: "./armTemplate.json" +uiDefinition: "./createUIDefinition.json" +registryServer: "nlbmarketplaceacrprod.azurecr.io" +extensionRegistrationParameters: + defaultScope: "cluster" + namespace: "nlk" diff --git a/charts/nlk/Chart.yaml b/charts/nlk/Chart.yaml index c11d885..b11ab86 100644 --- a/charts/nlk/Chart.yaml +++ b/charts/nlk/Chart.yaml @@ -1,19 +1,16 @@ --- apiVersion: v2 -appVersion: 0.1.0 -description: NGINX LoadBalancer for Kubernetes -name: nginx-loadbalancer-kubernetes -home: https://github.com/nginxinc/nginx-loadbalancer-kubernetes -icon: https://raw.githubusercontent.com/nginxinc/nginx-loadbalancer-kubernetes/main/nlk-logo.svg +appVersion: 0.8.0 +description: NGINXaaS LoadBalancer for Kubernetes +name: nginxaas-loadbalancer-kubernetes keywords: -- nginx -- loadbalancer -- ingress + - nginx + - nginxaas + - loadbalancer kubeVersion: '>= 1.22.0-0' maintainers: -- name: "@ciroque" -- name: "@chrisakker" -- name: "@abdennour" - + - name: "@ciroque" + - name: "@chrisakker" + - name: "@abdennour" type: application -version: 0.0.1 +version: 0.8.0 diff --git a/charts/nlk/templates/_helpers.tpl b/charts/nlk/templates/_helpers.tpl index 17a6405..27dbe94 100644 --- a/charts/nlk/templates/_helpers.tpl +++ b/charts/nlk/templates/_helpers.tpl @@ -48,6 +48,10 @@ Create chart name and version as used by the chart label. {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} +{{- define "nlk.apikeyname" -}} +{{- printf "%s-nginxaas-api-key" (include "nlk.fullname" .) | trunc 63 | trimSuffix "-" }} +{{- end }} + {{/* Common labels */}} @@ -76,11 +80,7 @@ app.kubernetes.io/instance: {{ .Release.Name }} Expand the name of the configmap. */}} {{- define "nlk.configName" -}} -{{- if .Values.nlk.customConfigMap -}} -{{ .Values.nlk.customConfigMap }} -{{- else -}} -{{- default (include "nlk.fullname" .) .Values.nlk.config.name -}} -{{- end -}} +{{- printf "%s-nlk-config" (include "nlk.fullname" .) | trunc 63 | trimSuffix "-" }} {{- end -}} {{/* @@ -91,14 +91,20 @@ Expand service account name. {{- end -}} {{- define "nlk.tag" -}} +{{- if .Values.global.azure -}} +{{- printf "%s" .Values.global.azure.images.nlk.tag -}} +{{- else -}} {{- default .Chart.AppVersion .Values.nlk.image.tag -}} {{- end -}} +{{- end -}} {{/* Expand image name. */}} {{- define "nlk.image" -}} -{{- if .Values.nlk.image.digest -}} +{{- if .Values.global.azure -}} +{{- printf "%s/%s:%s" .Values.global.azure.images.nlk.registry .Values.global.azure.images.nlk.image (include "nlk.tag" .) -}} +{{- else if .Values.nlk.image.digest -}} {{- printf "%s/%s@%s" .Values.nlk.image.registry .Values.nlk.image.repository .Values.nlk.image.digest -}} {{- else -}} {{- printf "%s/%s:%s" .Values.nlk.image.registry .Values.nlk.image.repository (include "nlk.tag" .) -}} diff --git a/charts/nlk/templates/clusterrole.yaml b/charts/nlk/templates/clusterrole.yaml index 4164475..47f95dd 100644 --- a/charts/nlk/templates/clusterrole.yaml +++ b/charts/nlk/templates/clusterrole.yaml @@ -7,12 +7,18 @@ rules: - apiGroups: - "" resources: - - configmaps - nodes - - secrets - services verbs: - get - list - watch + - apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - list + - watch {{- end }} diff --git a/charts/nlk/templates/clusterrolebinding.yaml b/charts/nlk/templates/clusterrolebinding.yaml index 0ccd455..8503a24 100644 --- a/charts/nlk/templates/clusterrolebinding.yaml +++ b/charts/nlk/templates/clusterrolebinding.yaml @@ -6,7 +6,7 @@ metadata: subjects: - kind: ServiceAccount name: {{ include "nlk.fullname" . }} - namespace: nlk + namespace: {{ .Release.Namespace }} roleRef: kind: ClusterRole name: {{ .Release.Namespace }}-{{ include "nlk.fullname" . }} diff --git a/charts/nlk/templates/dataplaneApiKey.yaml b/charts/nlk/templates/dataplaneApiKey.yaml new file mode 100644 index 0000000..2051138 --- /dev/null +++ b/charts/nlk/templates/dataplaneApiKey.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "nlk.apikeyname" . }} + namespace: {{ .Release.Namespace }} +type: Opaque +data: + nginxaasApiKey: {{ .Values.nlk.dataplaneApiKey | toString | b64enc }} diff --git a/charts/nlk/templates/nlk-configmap.yaml b/charts/nlk/templates/nlk-configmap.yaml index 482b8cb..0b6db57 100644 --- a/charts/nlk/templates/nlk-configmap.yaml +++ b/charts/nlk/templates/nlk-configmap.yaml @@ -1,14 +1,17 @@ apiVersion: v1 kind: ConfigMap metadata: - name: nlk-config - namespace: nlk + name: {{ include "nlk.configName" . }} + namespace: {{ .Release.Namespace }} data: -{{- if .Values.nlk.config.entries.hosts }} - nginx-hosts: "{{ .Values.nlk.config.entries.hosts }}" + config.yaml: | +{{- with .Values.nlk.config.logLevel }} + log-level: "{{ . }}" +{{- end }} +{{- with .Values.nlk.config.nginxHosts }} + nginx-hosts: {{ toJson . }} +{{- end }} + tls-mode: "{{ .Values.nlk.config.tls.mode }}" +{{- with .Values.nlk.config.serviceAnnotationMatch }} + service-annotation-match: "{{ . }}" {{- end }} - tls-mode: "{{ index .Values.nlk.defaultTLS "tls-mode" }}" - ca-certificate: "{{ index .Values.nlk.defaultTLS "ca-certificate" }}" - client-certificate: "{{ index .Values.nlk.defaultTLS "client-certificate" }}" - log-level: "{{ .Values.nlk.logLevel }}" - diff --git a/charts/nlk/templates/nlk-deployment.yaml b/charts/nlk/templates/nlk-deployment.yaml index fb55d77..93fcd38 100644 --- a/charts/nlk/templates/nlk-deployment.yaml +++ b/charts/nlk/templates/nlk-deployment.yaml @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "nlk.fullname" . }} - namespace: nlk + namespace: {{ .Release.Namespace }} labels: app: nlk spec: @@ -14,7 +14,16 @@ spec: metadata: labels: app: nlk +{{- if .Values.global.azure }} + azure-extensions-usage-release-identifier: {{ .Release.Name }} +{{- end }} + annotations: + checksum: {{ tpl (toYaml .Values.nlk) . | sha256sum }} spec: + {{- with .Values.nlk.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} containers: - name: {{ .Chart.Name }} image: {{ include "nlk.image" .}} @@ -41,4 +50,17 @@ spec: initialDelaySeconds: {{ .Values.nlk.readyStatus.initialDelaySeconds }} periodSeconds: {{ .Values.nlk.readyStatus.periodSeconds }} {{- end }} + env: + - name: NGINXAAS_DATAPLANE_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "nlk.apikeyname" . }} + key: nginxaasApiKey + volumeMounts: + - name: config + mountPath: /etc/nginxaas-loadbalancer-kubernetes serviceAccountName: {{ include "nlk.fullname" . }} + volumes: + - name: config + configMap: + name: {{ include "nlk.configName" . }} diff --git a/charts/nlk/templates/nlk-secret.yaml b/charts/nlk/templates/nlk-secret.yaml index ff7d7ff..cb96486 100644 --- a/charts/nlk/templates/nlk-secret.yaml +++ b/charts/nlk/templates/nlk-secret.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Secret metadata: name: {{ include "nlk.fullname" . }} - namespace: nlk + namespace: {{ .Release.Namespace }} annotations: kubernetes.io/service-account.name: {{ include "nlk.fullname" . }} type: kubernetes.io/service-account-token diff --git a/charts/nlk/templates/nlk-serviceaccount.yaml b/charts/nlk/templates/nlk-serviceaccount.yaml index 5bdca4f..d2cd8e4 100644 --- a/charts/nlk/templates/nlk-serviceaccount.yaml +++ b/charts/nlk/templates/nlk-serviceaccount.yaml @@ -3,5 +3,5 @@ apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "nlk.fullname" . }} - namespace: nlk + namespace: {{ .Release.Namespace }} {{- end }} diff --git a/charts/nlk/values.yaml b/charts/nlk/values.yaml index 394bc1f..1f2f1f2 100644 --- a/charts/nlk/values.yaml +++ b/charts/nlk/values.yaml @@ -1,124 +1,59 @@ +##################################### +# Global Azure Marketplace configuration for AKS integration. +# DO NOT REMOVE +global: + azure: + # images: + # nlk: + # registry: registry-1.docker.io + # image: nginx/nginxaas-loadbalancer-kubernetes + # tag: 0.4.0 +##################################### nlk: - name: nginx-loadbalancer-kubernetes - + name: nginxaas-loadbalancer-kubernetes kind: deployment - replicaCount: 1 - image: - registry: ghcr.io - repository: nginxinc/nginx-loadbalancer-kubernetes + registry: registry-1.docker.io + repository: nginx/nginxaas-loadbalancer-kubernetes pullPolicy: Always - # Overrides the image tag whose default is the chart appVersion. - tag: latest - + ## Overrides the image tag whose default is the chart appVersion. + # tag: 0.4.0 imagePullSecrets: [] nameOverride: "" fullnameOverride: "" - serviceAccount: - # Specifies whether a service account should be created + ## Specifies whether a service account should be created create: true - # Automatically mount a ServiceAccount's API credentials? + ## Automatically mount a ServiceAccount's API credentials? automount: true - # Annotations to add to the service account - annotations: {} - - podAnnotations: {} - podLabels: {} - - podSecurityContext: {} - # fsGroup: 2000 - - securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - - service: - type: ClusterIP - port: 80 - - ingress: - enabled: false - className: "" + ## Annotations to add to the service account annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - - resources: - requests: - cpu: 100m - memory: 128Mi - # limits: - # cpu: 100m - # memory: 128Mi - - autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 3 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - - # Additional volumes on the output Deployment definition. - volumes: [] - # - name: foo - # secret: - # secretName: mysecret - # optional: false - - # Additional volumeMounts on the output Deployment definition. - volumeMounts: [] - # - name: foo - # mountPath: "/etc/foo" - # readOnly: true - - nodeSelector: {} - - tolerations: [] - - affinity: {} - config: - entries: - hosts: - "http://10.1.1.4:9000/api,http://10.1.1.5:9000/api" - - defaultTLS: - tls-mode: "no-tls" - ca-certificate: "" - client-certificate: "" - - logLevel: "warn" - + ## trace,debug,info,warn,error,fatal,panic + logLevel: "info" + + ## the nginx hosts (comma-separated) to send upstream updates to + nginxHosts: "" + ## Sets the annotation value that NLK is looking for to watch a Service + # serviceAnnotationMatch: nginxaas + tls: + ## can also be set to "no-tls" to disable server cert verification + mode: "ca-tls" + ## Override with your own NGINXaaS dataplane API Key. + dataplaneApiKey: "test" containerPort: http: 51031 - liveStatus: enable: true port: 51031 initialDelaySeconds: 5 periodSeconds: 2 - readyStatus: enable: true port: 51031 initialDelaySeconds: 5 periodSeconds: 2 - rbac: ## Configures RBAC. create: true diff --git a/cmd/certificates-test-harness/doc.go b/cmd/certificates-test-harness/doc.go deleted file mode 100644 index 538bed9..0000000 --- a/cmd/certificates-test-harness/doc.go +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -/* -Package certificates_test_harness includes functionality boostrap and test the certification.Certificates implplementation. -*/ - -package main diff --git a/cmd/certificates-test-harness/main.go b/cmd/certificates-test-harness/main.go deleted file mode 100644 index 44d4a4e..0000000 --- a/cmd/certificates-test-harness/main.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/certification" - "github.com/sirupsen/logrus" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" - "path/filepath" -) - -func main() { - logrus.SetLevel(logrus.DebugLevel) - err := run() - if err != nil { - logrus.Fatal(err) - } -} - -func run() error { - logrus.Info("certificates-test-harness::run") - - ctx := context.Background() - var err error - - k8sClient, err := buildKubernetesClient() - if err != nil { - return fmt.Errorf(`error building a Kubernetes client: %w`, err) - } - - certificates := certification.NewCertificates(ctx, k8sClient) - - err = certificates.Initialize() - if err != nil { - return fmt.Errorf(`error occurred initializing certificates: %w`, err) - } - - go certificates.Run() - - <-ctx.Done() - return nil -} - -func buildKubernetesClient() (*kubernetes.Clientset, error) { - logrus.Debug("Watcher::buildKubernetesClient") - - var kubeconfig *string - var k8sConfig *rest.Config - - k8sConfig, err := rest.InClusterConfig() - if errors.Is(err, rest.ErrNotInCluster) { - if home := homedir.HomeDir(); home != "" { - path := filepath.Join(home, ".kube", "config") - kubeconfig = &path - - k8sConfig, err = clientcmd.BuildConfigFromFlags("", *kubeconfig) - if err != nil { - return nil, fmt.Errorf(`error occurred building the kubeconfig: %w`, err) - } - } else { - return nil, fmt.Errorf(`not running in a Cluster: %w`, err) - } - } else if err != nil { - return nil, fmt.Errorf(`error occurred getting the Cluster config: %w`, err) - } - - client, err := kubernetes.NewForConfig(k8sConfig) - if err != nil { - return nil, fmt.Errorf(`error occurred creating a client: %w`, err) - } - return client, nil -} diff --git a/cmd/configuration-test-harness/doc.go b/cmd/configuration-test-harness/doc.go deleted file mode 100644 index 06ab7d0..0000000 --- a/cmd/configuration-test-harness/doc.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/cmd/configuration-test-harness/main.go b/cmd/configuration-test-harness/main.go deleted file mode 100644 index 56e8b5d..0000000 --- a/cmd/configuration-test-harness/main.go +++ /dev/null @@ -1,80 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - configuration2 "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/sirupsen/logrus" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" - "path/filepath" -) - -func main() { - logrus.SetLevel(logrus.DebugLevel) - err := run() - if err != nil { - logrus.Fatal(err) - } -} - -func run() error { - logrus.Info("configuration-test-harness::run") - - ctx := context.Background() - var err error - - k8sClient, err := buildKubernetesClient() - if err != nil { - return fmt.Errorf(`error building a Kubernetes client: %w`, err) - } - - configuration, err := configuration2.NewSettings(ctx, k8sClient) - if err != nil { - return fmt.Errorf(`error occurred creating configuration: %w`, err) - } - - err = configuration.Initialize() - if err != nil { - return fmt.Errorf(`error occurred initializing configuration: %w`, err) - } - - go configuration.Run() - - <-ctx.Done() - - return err -} - -func buildKubernetesClient() (*kubernetes.Clientset, error) { - logrus.Debug("Watcher::buildKubernetesClient") - - var kubeconfig *string - var k8sConfig *rest.Config - - k8sConfig, err := rest.InClusterConfig() - if errors.Is(err, rest.ErrNotInCluster) { - if home := homedir.HomeDir(); home != "" { - path := filepath.Join(home, ".kube", "config") - kubeconfig = &path - - k8sConfig, err = clientcmd.BuildConfigFromFlags("", *kubeconfig) - if err != nil { - return nil, fmt.Errorf(`error occurred building the kubeconfig: %w`, err) - } - } else { - return nil, fmt.Errorf(`not running in a Cluster: %w`, err) - } - } else if err != nil { - return nil, fmt.Errorf(`error occurred getting the Cluster config: %w`, err) - } - - client, err := kubernetes.NewForConfig(k8sConfig) - if err != nil { - return nil, fmt.Errorf(`error occurred creating a client: %w`, err) - } - return client, nil -} diff --git a/cmd/nginx-loadbalancer-kubernetes/main.go b/cmd/nginx-loadbalancer-kubernetes/main.go index 531076e..43de794 100644 --- a/cmd/nginx-loadbalancer-kubernetes/main.go +++ b/cmd/nginx-loadbalancer-kubernetes/main.go @@ -8,23 +8,27 @@ package main import ( "context" "fmt" + "log/slog" "os" "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" "github.com/nginxinc/kubernetes-nginx-ingress/internal/observation" "github.com/nginxinc/kubernetes-nginx-ingress/internal/probation" "github.com/nginxinc/kubernetes-nginx-ingress/internal/synchronization" - "github.com/sirupsen/logrus" + "github.com/nginxinc/kubernetes-nginx-ingress/internal/translation" + "github.com/nginxinc/kubernetes-nginx-ingress/pkg/buildinfo" + "golang.org/x/sync/errgroup" + "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/workqueue" ) func main() { err := run() if err != nil { - logrus.Fatal(err) + slog.Error(err.Error()) + os.Exit(1) } } @@ -37,96 +41,104 @@ func run() error { return fmt.Errorf(`error building a Kubernetes client: %w`, err) } - settings, err := configuration.NewSettings(ctx, k8sClient) + settings, err := configuration.Read("config.yaml", "/etc/nginxaas-loadbalancer-kubernetes") if err != nil { - return fmt.Errorf(`error occurred creating settings: %w`, err) + return fmt.Errorf(`error occurred accessing configuration: %w`, err) } - err = settings.Initialize() - if err != nil { - return fmt.Errorf(`error occurred initializing settings: %w`, err) - } + initializeLogger(settings.LogLevel) - go settings.Run() + synchronizerWorkqueue := buildWorkQueue(settings.Synchronizer.WorkQueueSettings) - synchronizerWorkqueue, err := buildWorkQueue(settings.Synchronizer.WorkQueueSettings) - if err != nil { - return fmt.Errorf(`error occurred building a workqueue: %w`, err) - } + factory := informers.NewSharedInformerFactoryWithOptions( + k8sClient, settings.Watcher.ResyncPeriod, + ) - synchronizer, err := synchronization.NewSynchronizer(settings, synchronizerWorkqueue) - if err != nil { - return fmt.Errorf(`error initializing synchronizer: %w`, err) - } + serviceInformer := factory.Core().V1().Services() + endpointSliceInformer := factory.Discovery().V1().EndpointSlices() + endpointSliceLister := endpointSliceInformer.Lister() + nodesInformer := factory.Core().V1().Nodes() + nodesLister := nodesInformer.Lister() + + translator := translation.NewTranslator(endpointSliceLister, nodesLister) - handlerWorkqueue, err := buildWorkQueue(settings.Synchronizer.WorkQueueSettings) + synchronizer, err := synchronization.NewSynchronizer( + settings, synchronizerWorkqueue, translator, serviceInformer.Lister()) if err != nil { - return fmt.Errorf(`error occurred building a workqueue: %w`, err) + return fmt.Errorf(`error initializing synchronizer: %w`, err) } - handler := observation.NewHandler(settings, synchronizer, handlerWorkqueue) - - watcher, err := observation.NewWatcher(settings, handler) + watcher, err := observation.NewWatcher(settings, synchronizer, serviceInformer, endpointSliceInformer, nodesInformer) if err != nil { return fmt.Errorf(`error occurred creating a watcher: %w`, err) } - err = watcher.Initialize() - if err != nil { - return fmt.Errorf(`error occurred initializing the watcher: %w`, err) + factory.Start(ctx.Done()) + results := factory.WaitForCacheSync(ctx.Done()) + for name, success := range results { + if !success { + return fmt.Errorf(`error occurred waiting for cache sync for %s`, name) + } } - go handler.Run(ctx.Done()) - go synchronizer.Run(ctx.Done()) + g, ctx := errgroup.WithContext(ctx) + + g.Go(func() error { return synchronizer.Run(ctx) }) probeServer := probation.NewHealthServer() probeServer.Start() - err = watcher.Watch() - if err != nil { - return fmt.Errorf(`error occurred watching for events: %w`, err) + g.Go(func() error { return watcher.Run(ctx) }) + + err = g.Wait() + return err +} + +func initializeLogger(logLevel string) { + programLevel := new(slog.LevelVar) + + switch logLevel { + case "error": + programLevel.Set(slog.LevelError) + case "warn": + programLevel.Set(slog.LevelWarn) + case "info": + programLevel.Set(slog.LevelInfo) + case "debug": + programLevel.Set(slog.LevelDebug) + default: + programLevel.Set(slog.LevelWarn) } - <-ctx.Done() - return nil + handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel}) + logger := slog.New(handler).With("version", buildinfo.SemVer()) + slog.SetDefault(logger) + slog.Debug("Settings::setLogLevel", slog.String("level", logLevel)) } -// buildKubernetesClient builds a Kubernetes clientset, supporting both in-cluster and out-of-cluster (kubeconfig) configurations. func buildKubernetesClient() (*kubernetes.Clientset, error) { - var config *rest.Config - var err error - - // Try in-cluster config first - config, err = rest.InClusterConfig() - if err != nil { - if err == rest.ErrNotInCluster { - // Not running in a cluster, fall back to kubeconfig - kubeconfigPath := os.Getenv("KUBECONFIG") - if kubeconfigPath == "" { - kubeconfigPath = clientcmd.RecommendedHomeFile // ~/.kube/config - } - - config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) - if err != nil { - return nil, fmt.Errorf("could not get Kubernetes config: %w", err) - } - } else { - return nil, fmt.Errorf("error occurred getting the in-cluster config: %w", err) - } + slog.Debug("Watcher::buildKubernetesClient") + k8sConfig, err := rest.InClusterConfig() + if err == rest.ErrNotInCluster { + return nil, fmt.Errorf(`not running in a Cluster: %w`, err) + } else if err != nil { + return nil, fmt.Errorf(`error occurred getting the Cluster config: %w`, err) } - // Create the clientset - client, err := kubernetes.NewForConfig(config) + client, err := kubernetes.NewForConfig(k8sConfig) if err != nil { - return nil, fmt.Errorf("error occurred creating a Kubernetes client: %w", err) + return nil, fmt.Errorf(`error occurred creating a client: %w`, err) } return client, nil } -func buildWorkQueue(settings configuration.WorkQueueSettings) (workqueue.RateLimitingInterface, error) { - logrus.Debug("Watcher::buildSynchronizerWorkQueue") +func buildWorkQueue(settings configuration.WorkQueueSettings, +) workqueue.TypedRateLimitingInterface[synchronization.ServiceKey] { + slog.Debug("Watcher::buildSynchronizerWorkQueue") - rateLimiter := workqueue.NewItemExponentialFailureRateLimiter(settings.RateLimiterBase, settings.RateLimiterMax) - return workqueue.NewNamedRateLimitingQueue(rateLimiter, settings.Name), nil + rateLimiter := workqueue.NewTypedItemExponentialFailureRateLimiter[synchronization.ServiceKey]( + settings.RateLimiterBase, settings.RateLimiterMax) + return workqueue.NewTypedRateLimitingQueueWithConfig( + rateLimiter, workqueue.TypedRateLimitingQueueConfig[synchronization.ServiceKey]{Name: settings.Name}) } diff --git a/cmd/tls-config-factory-test-harness/doc.go b/cmd/tls-config-factory-test-harness/doc.go deleted file mode 100644 index 06ab7d0..0000000 --- a/cmd/tls-config-factory-test-harness/doc.go +++ /dev/null @@ -1 +0,0 @@ -package main diff --git a/cmd/tls-config-factory-test-harness/main.go b/cmd/tls-config-factory-test-harness/main.go deleted file mode 100644 index 3f46d4f..0000000 --- a/cmd/tls-config-factory-test-harness/main.go +++ /dev/null @@ -1,230 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/authentication" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/certification" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - "github.com/sirupsen/logrus" - "os" -) - -const ( - CaCertificateSecretKey = "nlk-tls-ca-secret" - ClientCertificateSecretKey = "nlk-tls-client-secret" -) - -type TlsConfiguration struct { - Description string - Settings configuration.Settings -} - -func main() { - logrus.SetLevel(logrus.DebugLevel) - - configurations := buildConfigMap() - - for name, settings := range configurations { - fmt.Print("\033[H\033[2J") - - logrus.Infof("\n\n\t*** Building TLS config for <<< %s >>>\n\n", name) - - tlsConfig, err := authentication.NewTlsConfig(&settings.Settings) - if err != nil { - panic(err) - } - - rootCaCount := 0 - certificateCount := 0 - - if tlsConfig.RootCAs != nil { - rootCaCount = len(tlsConfig.RootCAs.Subjects()) - } - - if tlsConfig.Certificates != nil { - certificateCount = len(tlsConfig.Certificates) - } - - logrus.Infof("Successfully built TLS config: \n\tDescription: %s \n\tRootCA count: %v\n\tCertificate count: %v", settings.Description, rootCaCount, certificateCount) - - bufio.NewReader(os.Stdin).ReadBytes('\n') - } - - fmt.Print("\033[H\033[2J") - logrus.Infof("\n\n\t*** All done! ***\n\n") -} - -func buildConfigMap() map[string]TlsConfiguration { - configurations := make(map[string]TlsConfiguration) - - configurations["ss-tls"] = TlsConfiguration{ - Description: "Self-signed TLS requires just a CA certificate", - Settings: ssTlsConfig(), - } - - configurations["ss-mtls"] = TlsConfiguration{ - Description: "Self-signed mTLS requires a CA certificate and a client certificate", - Settings: ssMtlsConfig(), - } - - configurations["ca-tls"] = TlsConfiguration{ - Description: "CA TLS requires no certificates", - Settings: caTlsConfig(), - } - - configurations["ca-mtls"] = TlsConfiguration{ - Description: "CA mTLS requires a client certificate", - Settings: caMtlsConfig(), - } - - return configurations -} - -func ssTlsConfig() configuration.Settings { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(caCertificatePEM()) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), clientCertificatePEM()) - - return configuration.Settings{ - TlsMode: configuration.SelfSignedTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - }, - } -} - -func ssMtlsConfig() configuration.Settings { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(caCertificatePEM()) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), clientCertificatePEM()) - - return configuration.Settings{ - TlsMode: configuration.SelfSignedMutualTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - }, - } -} - -func caTlsConfig() configuration.Settings { - return configuration.Settings{ - TlsMode: configuration.CertificateAuthorityTLS, - } -} - -func caMtlsConfig() configuration.Settings { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), clientCertificatePEM()) - - return configuration.Settings{ - TlsMode: configuration.CertificateAuthorityMutualTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - }, - } -} - -func caCertificatePEM() string { - return ` ------BEGIN CERTIFICATE----- -MIIDTzCCAjcCFA4Zdj3E9TdjOP48eBRDGRLfkj7CMA0GCSqGSIb3DQEBCwUAMGQx -CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0 -dGxlMQ4wDAYDVQQKDAVOR0lOWDEeMBwGA1UECwwVQ29tbXVuaXR5ICYgQWxsaWFu -Y2VzMB4XDTIzMDkyOTE3MTY1MVoXDTIzMTAyOTE3MTY1MVowZDELMAkGA1UEBhMC -VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxDjAMBgNV -BAoMBU5HSU5YMR4wHAYDVQQLDBVDb21tdW5pdHkgJiBBbGxpYW5jZXMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwlI4ZvJ/6hvqULFVL+1ZSRDTPQ48P -umehJhPz6xPhC9UkeTe2FZxm2Rsi1I5QXm/bTG2OcX775jgXzae9NQjctxwrz4Ks -LOWUvRkkfhQR67xk0Noux76/9GWGnB+Fapn54tlWql6uHQfOu1y7MCRkZ27zHbkk -lq4Oa2RmX8rIyECWgbTyL0kETBVJU8bYORQ5JjhRlz08inq3PggY8blrehIetrWN -dw+gzcqdvAI2uSCodHTHM/77KipnYmPiSiDjSDRlXdxTG8JnyIB78IoH/sw6RyBm -CvVa3ytvKziXAvbBoXq5On5WmMRF97p/MmBc53ExMuDZjA4fisnViS0PAgMBAAEw -DQYJKoZIhvcNAQELBQADggEBAJeoa2P59zopLjBInx/DnWn1N1CmFLb0ejKxG2jh -cOw15Sx40O0XrtrAto38iu4R/bkBeNCSUILlT+A3uYDila92Dayvls58WyIT3meD -G6+Sx/QDF69+4AXpVy9mQ+hxcofpFA32+GOMXwmk2OrAcdSkkGSBhZXgvTpQ64dl -xSiQ5EQW/K8LoBoEOXfjIZJNPORgKn5MI09AY7/47ycKDKTUU2yO8AtIHYKttw0x -kfIg7QOdo1F9IXVpGjJI7ynyrgsCEYxMoDyH42Dq84eKgrUFLEXemEz8hgdFgK41 -0eUYhAtzWHbRPBp+U/34CQoZ5ChNFp2YipvtXrzKE8KLkuM= ------END CERTIFICATE----- -` -} - -func clientCertificatePEM() string { - return ` ------BEGIN CERTIFICATE----- -MIIEDDCCAvSgAwIBAgIULDFXwGrTohN/PRao2rSLk9VxFdgwDQYJKoZIhvcNAQEL -BQAwXTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcM -CUluZGlhbm9sYTEPMA0GA1UECgwGV2FnbmVyMRQwEgYDVQQLDAtEZXZlbG9wbWVu -dDAeFw0yMzA5MjkxNzA3NTRaFw0yNDA5MjgxNzA3NTRaMGQxCzAJBgNVBAYTAlVT -MRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMQ4wDAYDVQQK -DAVOR0lOWDEeMBwGA1UECwwVQ29tbXVuaXR5ICYgQWxsaWFuY2VzMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqNuEZ6+TcFrmzcwp8u8mzk0jPd47GKk -H9wwdkFCzGdd8KJkFQhzLyimZIWkRDYmhaxZd76jKGBpdfyivR4e4Mi5WYlpPGMI -ppM7/rMYP8yn04tkokAazbqjOTlF8NUKqGQwqAN4Z/PvoG2HyP9omGpuLWTbjKto -oGr5aPBIhzlICU3OjHn6eKaekJeAYBo3uQFYOxCjtE9hJLDOY4q7zomMJfYoeoA2 -Afwkx1Lmozp2j/esB52/HlCKVhAOzZsPzM+E9eb1Q722dUed4OuiVYSfrDzeImrA -TufzTBTMEpFHCtdBGocZ3LRd9qmcP36ZCMsJNbYnQZV3XsI4JhjjHwIDAQABo4G8 -MIG5MBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBRDl4jeiE1mJDPrYmQx -g2ndkWxpYjCBggYDVR0jBHsweaFhpF8wXTELMAkGA1UEBhMCVVMxEzARBgNVBAgM -Cldhc2hpbmd0b24xEjAQBgNVBAcMCUluZGlhbm9sYTEPMA0GA1UECgwGV2FnbmVy -MRQwEgYDVQQLDAtEZXZlbG9wbWVudIIUNxx2Mr+PKXiF3d2i51fb/rnWbBgwDQYJ -KoZIhvcNAQELBQADggEBAL0wS6LkFuqGDlhaTGnAXRwRDlC6uwrm8wNWppaw9Vqt -eaZGFzodcCFp9v8jjm1LsTv7gEUBnWtn27LGP4GJSpZjiq6ulJypBxo/G0OkMByK -ky4LeGY7/BQzjzHdfXEq4gwfC45ni4n54uS9uzW3x+AwLSkxPtBxSwxhtwBLo9aE -Ql4rHUoWc81mhGO5mMZBaorxZXps1f3skfP+wZX943FIMt5gz4hkxwFp3bI/FrqH -R8DLUlCzBA9+7WIFD1wi25TV+Oyq3AjT/KiVmR+umrukhnofCWe8JiVpb5iJcd2k -Rc7+bvyb5OCnJdEX08XGWmF2/OFKLrCzLH1tQxk7VNE= ------END CERTIFICATE----- -` -} - -// clientKeyPEM returns a PEM-encoded client key. -// Note: The key is self-signed and generated explicitly for tests, -// it is not used anywhere else. -func clientKeyPEM() string { - return ` ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCio24Rnr5NwWub -NzCny7ybOTSM93jsYqQf3DB2QULMZ13womQVCHMvKKZkhaRENiaFrFl3vqMoYGl1 -/KK9Hh7gyLlZiWk8Ywimkzv+sxg/zKfTi2SiQBrNuqM5OUXw1QqoZDCoA3hn8++g -bYfI/2iYam4tZNuMq2igavlo8EiHOUgJTc6Mefp4pp6Ql4BgGje5AVg7EKO0T2Ek -sM5jirvOiYwl9ih6gDYB/CTHUuajOnaP96wHnb8eUIpWEA7Nmw/Mz4T15vVDvbZ1 -R53g66JVhJ+sPN4iasBO5/NMFMwSkUcK10EahxnctF32qZw/fpkIywk1tidBlXde -wjgmGOMfAgMBAAECggEAA+R2b2yFsHW3HhVhkDqDjpF9bPxFRB8OP4b1D/d64kp9 -CJPSYmB75T6LUO+T4WAMZvmbgI6q9/3quDyuJmmQop+bNAXiY2QZYmc2sd9Wbrx2 -rczxwSJYoeDcJDP3NQ7cPPB866B9ortHWmcUr15RgghWD7cQvBqkG+bDhlvt2HKg -NZmL6R0U1bVAlRMtFJiEdMHuGnPmoDU5IGc1fKjsgijLeMboUrEaXWINoEm8ii5e -/mnsfLCBmeJAsKuXxL8/1UmvWYE/ltDfYBVclKhcH2UWTZv7pdRtHnu49lkZivUB -ZvH2DHsSMjXj6+HHr6RcRGmnMDyfhJFPCjOdTjf4oQKBgQDeYLWZx22zGXgfb7md -MhdKed9GxMJHzs4jDouqrHy0w95vwMi7RXgeKpKXiCruqSEB/Trtq01f7ekh0mvJ -Ys0h4A5tkrT5BVVBs+65uF/kSF2z/CYGNRhAABO7UM+B1e3tlnjfjeb/M78IcFbT -FyBN90A/+a9JGZ4obt3ack3afwKBgQC7OncnXC9L5QCWForJWQCNO3q3OW1Gaoxe -OAnmnPSJ7NUd7xzDNE8pzBUWXysZCoRU3QNElcQfzHWtZx1iqJPk3ERK2awNsnV7 -X2Fu4vHzIr5ZqVnM8NG7+iWrxRLf+ctcEvPiqRYo+g+r5tTGJqWh2nh9W7iQwwwE -1ikoxFBnYQKBgCbDdOR5fwXZSrcwIorkUGsLE4Cii7s4sXYq8u2tY4+fFQcl89ex -JF8dzK/dbJ5tnPNb0Qnc8n/mWN0scN2J+3gMNnejOyitZU8urk5xdUW115+oNHig -iLmfSdE9JO7c+7yOnkNZ2QpjWsl9y6TAQ0FT+D8upv93F7q0mLebdTbBAoGBALmp -r5EThD9RlvQ+5F/oZ3imO/nH88n5TLr9/St4B7NibLAjdrVIgRwkqeCmfRl26WUy -SdRQY81YtnU/JM+59fbkSsCi/FAU4RV3ryoD2QRPNs249zkYshMjawncAuyiS/xB -OyJQpI3782B3JhZdKrDG8eb19p9vG9MMAILRsh3hAoGASCvmq10nHHGFYTerIllQ -sohNaw3KDlQTkpyOAztS4jOXwvppMXbYuCznuJbHz0NEM2ww+SiA1RTvD/gosYYC -mMgqRga/Qu3b149M3wigDjK+RAcyuNGZN98bqU/UjJLjqH6IMutt59+9XNspcD96 -z/3KkMx4uqJXZyvQrmkolSg= ------END PRIVATE KEY----- -` -} - -func buildClientCertificateEntry(keyPEM, certificatePEM string) map[string]core.SecretBytes { - return map[string]core.SecretBytes{ - certification.CertificateKey: core.SecretBytes([]byte(certificatePEM)), - certification.CertificateKeyKey: core.SecretBytes([]byte(keyPEM)), - } -} - -func buildCaCertificateEntry(certificatePEM string) map[string]core.SecretBytes { - return map[string]core.SecretBytes{ - certification.CertificateKey: core.SecretBytes([]byte(certificatePEM)), - } -} diff --git a/deployments/checks/sample-ingress-mod.yaml b/deployments/checks/sample-ingress-mod.yaml deleted file mode 100644 index cd79305..0000000 --- a/deployments/checks/sample-ingress-mod.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: example-ingress - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /$1 -spec: - rules: - - host: hello-world.net - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: web - port: - number: 8080 \ No newline at end of file diff --git a/deployments/checks/sample-ingress.yaml b/deployments/checks/sample-ingress.yaml deleted file mode 100644 index 7ff7fc5..0000000 --- a/deployments/checks/sample-ingress.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: example-ingress - annotations: - nginx.ingress.kubernetes.io/rewrite-target: /$1 -spec: - rules: - - host: hello-world.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: web - port: - number: 8080 \ No newline at end of file diff --git a/deployments/deployment/configmap.yaml b/deployments/deployment/configmap.yaml deleted file mode 100644 index fd30dbe..0000000 --- a/deployments/deployment/configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -data: - nginx-hosts: "https://10.0.0.1:9000/api" - tls-mode: "no-tls" - ca-certificate: "" - client-certificate: "" - log-level: "warn" -metadata: - name: nlk-config - namespace: nlk diff --git a/deployments/deployment/deployment.yaml b/deployments/deployment/deployment.yaml deleted file mode 100644 index 4c871c2..0000000 --- a/deployments/deployment/deployment.yaml +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nlk-deployment - namespace: nlk - labels: - app: nlk -spec: - replicas: 1 - selector: - matchLabels: - app: nlk - template: - metadata: - labels: - app: nlk - spec: - containers: - - name: nginx-loadbalancer-kubernetes - image: ghcr.io/nginxinc/nginx-loadbalancer-kubernetes:latest - imagePullPolicy: Always - ports: - - name: http - containerPort: 51031 - protocol: TCP - livenessProbe: - httpGet: - path: /livez - port: 51031 - initialDelaySeconds: 5 - periodSeconds: 2 - readinessProbe: - httpGet: - path: /readyz - port: 51031 - initialDelaySeconds: 5 - periodSeconds: 2 - serviceAccountName: nginx-loadbalancer-kubernetes diff --git a/deployments/deployment/namespace.yaml b/deployments/deployment/namespace.yaml deleted file mode 100644 index 8f9e382..0000000 --- a/deployments/deployment/namespace.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: nlk - labels: - name: nlk diff --git a/deployments/rbac/apply.sh b/deployments/rbac/apply.sh deleted file mode 100755 index 58248da..0000000 --- a/deployments/rbac/apply.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -pushd "$(dirname "$0")" - -echo "Applying all RBAC resources..." - -kubectl apply -f serviceaccount.yaml -kubectl apply -f clusterrole.yaml -kubectl apply -f clusterrolebinding.yaml -kubectl apply -f secret.yaml - -popd diff --git a/deployments/rbac/clusterrole.yaml b/deployments/rbac/clusterrole.yaml deleted file mode 100644 index c50bed8..0000000 --- a/deployments/rbac/clusterrole.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: resource-get-watch-list - namespace: nlk -rules: - - apiGroups: - - "" - resources: ["services", "nodes", "configmaps", "secrets"] - verbs: ["get", "watch", "list"] diff --git a/deployments/rbac/clusterrolebinding.yaml b/deployments/rbac/clusterrolebinding.yaml deleted file mode 100644 index d48ffb8..0000000 --- a/deployments/rbac/clusterrolebinding.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: "nginx-loadbalancer-kubernetes:resource-get-watch-list" - namespace: nlk -subjects: - - kind: ServiceAccount - name: nginx-loadbalancer-kubernetes - namespace: nlk -roleRef: - kind: ClusterRole - name: resource-get-watch-list - apiGroup: rbac.authorization.k8s.io diff --git a/deployments/rbac/secret.yaml b/deployments/rbac/secret.yaml deleted file mode 100644 index 71576bf..0000000 --- a/deployments/rbac/secret.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: nginx-loadbalancer-kubernetes-secret - namespace: nlk - annotations: - kubernetes.io/service-account.name: nginx-loadbalancer-kubernetes -type: kubernetes.io/service-account-token diff --git a/deployments/rbac/serviceaccount.yaml b/deployments/rbac/serviceaccount.yaml deleted file mode 100644 index 76f238c..0000000 --- a/deployments/rbac/serviceaccount.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: nginx-loadbalancer-kubernetes - namespace: nlk diff --git a/deployments/rbac/unapply.sh b/deployments/rbac/unapply.sh deleted file mode 100755 index f29f90d..0000000 --- a/deployments/rbac/unapply.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo "Unapplying all RBAC resources..." - -kubectl delete -f serviceaccount.yaml -kubectl delete -f clusterrole.yaml -kubectl delete -f clusterrolebinding.yaml -kubectl delete -f secret.yaml diff --git a/doc.go b/doc.go index 7c97bd2..1034f14 100644 --- a/doc.go +++ b/doc.go @@ -3,4 +3,4 @@ * Use of this source code is governed by the Apache License that can be found in the LICENSE file. */ -package kubernetes_nginx_ingress +package kubernetesnginxingress diff --git a/docker-user b/docker-user new file mode 100644 index 0000000..65be48a --- /dev/null +++ b/docker-user @@ -0,0 +1 @@ +nginx:x:101:101:nginx:/var/cache/nginx:/sbin/nologin \ No newline at end of file diff --git a/go.mod b/go.mod index 7664a62..6a78fe2 100644 --- a/go.mod +++ b/go.mod @@ -4,57 +4,71 @@ module github.com/nginxinc/kubernetes-nginx-ingress -go 1.23.3 +go 1.24.4 require ( - github.com/sirupsen/logrus v1.9.3 - k8s.io/api v0.31.2 - k8s.io/apimachinery v0.31.2 - k8s.io/client-go v0.31.2 - github.com/nginxinc/nginx-plus-go-client/v2 v2.0.1 + github.com/nginx/nginx-plus-go-client/v2 v2.4.0 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.10.0 + golang.org/x/sync v0.13.0 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 073139f..8cf5ed8 100644 --- a/go.sum +++ b/go.sum @@ -5,37 +5,38 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -49,8 +50,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -58,81 +63,105 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nginxinc/nginx-plus-go-client/v2 v2.0.1 h1:5VVK38bnELMDWnwfF6dSv57ResXh9AUzeDa72ENj94o= -github.com/nginxinc/nginx-plus-go-client/v2 v2.0.1/go.mod h1:He+1izxYxVVO5/C9ZTukwOpvkAx5eS19nRQgKXDhX5I= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/nginx/nginx-plus-go-client/v2 v2.4.0 h1:4c7V57CLCZUOxQCUcS9G8a5MClzdmxByBm+f4zKMzAY= +github.com/nginx/nginx-plus-go-client/v2 v2.4.0/go.mod h1:P+dIP2oKYzFoyf/zlLWQa8Sf+fHb+CclOKzxAjxpvug= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -140,27 +169,29 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= -k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= -k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= -k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc= -k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= -k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= -k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/application/application_common_test.go b/internal/application/application_common_test.go index e963d03..c42bc04 100644 --- a/internal/application/application_common_test.go +++ b/internal/application/application_common_test.go @@ -7,6 +7,7 @@ package application import ( "errors" + "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" ) @@ -18,11 +19,11 @@ const ( server = "server" ) -func buildTerrorizingBorderClient(clientType string) (Interface, *mocks.MockNginxClient, error) { +func buildTerrorizingBorderClient(clientType string) (Interface, error) { nginxClient := mocks.NewErroringMockClient(errors.New(`something went horribly horribly wrong`)) bc, err := NewBorderClient(clientType, nginxClient) - return bc, nginxClient, err + return bc, err } func buildBorderClient(clientType string) (Interface, *mocks.MockNginxClient, error) { diff --git a/internal/application/application_constants.go b/internal/application/application_constants.go index 0ec1826..4cb23a5 100644 --- a/internal/application/application_constants.go +++ b/internal/application/application_constants.go @@ -12,7 +12,8 @@ package application // annotations: // nginxinc.io/nlk-: // -// where is the name of the upstream in the NGINX Plus configuration and is one of the constants below. +// where is the name of the upstream in the NGINX Plus configuration +// and is one of the constants below. // // Note, this is an extensibility point. To add a Border Server client... // 1. Create a module that implements the BorderClient interface; @@ -23,6 +24,6 @@ const ( // ClientTypeNginxStream creates a NginxStreamBorderClient that uses the Stream* methods of the NGINX Plus client. ClientTypeNginxStream = "stream" - // ClientTypeNginxHttp creates an NginxHttpBorderClient that uses the HTTP* methods of the NGINX Plus client. - ClientTypeNginxHttp = "http" + // ClientTypeNginxHTTP creates an NginxHTTPBorderClient that uses the HTTP* methods of the NGINX Plus client. + ClientTypeNginxHTTP = "http" ) diff --git a/internal/application/border_client.go b/internal/application/border_client.go index a5cc93e..8bca843 100644 --- a/internal/application/border_client.go +++ b/internal/application/border_client.go @@ -6,20 +6,21 @@ package application import ( + "context" "fmt" + "log/slog" + "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - "github.com/sirupsen/logrus" ) // Interface defines the functions required to implement a Border Client. type Interface interface { - Update(*core.ServerUpdateEvent) error - Delete(*core.ServerUpdateEvent) error + Update(context.Context, *core.ServerUpdateEvent) error + Delete(context.Context, *core.ServerUpdateEvent) error } // BorderClient defines any state need by the Border Client. -type BorderClient struct { -} +type BorderClient struct{} // NewBorderClient is the Factory function for creating a Border Client. // @@ -28,14 +29,14 @@ type BorderClient struct { // 2. Add a new constant in application_constants.go that acts as a key for selecting the client; // 3. Update the NewBorderClient factory method in border_client.go that returns the client; func NewBorderClient(clientType string, borderClient interface{}) (Interface, error) { - logrus.Debugf(`NewBorderClient for type: %s`, clientType) + slog.Debug("NewBorderClient", slog.String("client", clientType)) switch clientType { case ClientTypeNginxStream: return NewNginxStreamBorderClient(borderClient) - case ClientTypeNginxHttp: - return NewNginxHttpBorderClient(borderClient) + case ClientTypeNginxHTTP: + return NewNginxHTTPBorderClient(borderClient) default: borderClient, _ := NewNullBorderClient() diff --git a/internal/application/border_client_test.go b/internal/application/border_client_test.go index 0b8105e..8960eee 100644 --- a/internal/application/border_client_test.go +++ b/internal/application/border_client_test.go @@ -6,23 +6,26 @@ package application import ( - "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" "testing" + + "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" ) func TestBorderClient_CreatesHttpBorderClient(t *testing.T) { + t.Parallel() borderClient := mocks.MockNginxClient{} client, err := NewBorderClient("http", borderClient) if err != nil { t.Errorf(`error creating border client: %v`, err) } - if _, ok := client.(*NginxHttpBorderClient); !ok { - t.Errorf(`expected client to be of type NginxHttpBorderClient`) + if _, ok := client.(*NginxHTTPBorderClient); !ok { + t.Errorf(`expected client to be of type NginxHTTPBorderClient`) } } func TestBorderClient_CreatesTcpBorderClient(t *testing.T) { + t.Parallel() borderClient := mocks.MockNginxClient{} client, err := NewBorderClient("stream", borderClient) if err != nil { @@ -35,6 +38,7 @@ func TestBorderClient_CreatesTcpBorderClient(t *testing.T) { } func TestBorderClient_UnknownClientType(t *testing.T) { + t.Parallel() unknownClientType := "unknown" borderClient := mocks.MockNginxClient{} client, err := NewBorderClient(unknownClientType, borderClient) diff --git a/internal/application/doc.go b/internal/application/doc.go index 34c27d0..296cb67 100644 --- a/internal/application/doc.go +++ b/internal/application/doc.go @@ -17,7 +17,7 @@ To add a Border Server client... At this time the only supported Border Servers are NGINX Plus servers. The two Border Server clients for NGINX Plus are: -- NginxHttpBorderClient: updates NGINX Plus servers using HTTP Upstream methods on the NGINX Plus API. +- NginxHTTPBorderClient: updates NGINX Plus servers using HTTP Upstream methods on the NGINX Plus API. - NginxStreamBorderClient: updates NGINX Plus servers using Stream Upstream methods on the NGINX Plus API. Both of these implementations use the NGINX Plus client module to communicate with the NGINX Plus server. @@ -27,7 +27,8 @@ Selection of the appropriate client is based on the Annotations present on the S annotations: nginxinc.io/nlk-: -where is the name of the upstream in the NGINX Plus configuration and is one of the constants in application_constants.go. +where is the name of the upstream in the NGINX Plus configuration +and is one of the constants in application_constants.go. */ package application diff --git a/internal/application/nginx_client_interface.go b/internal/application/nginx_client_interface.go index cb8fb4c..1a60c5e 100644 --- a/internal/application/nginx_client_interface.go +++ b/internal/application/nginx_client_interface.go @@ -8,20 +8,31 @@ package application import ( "context" - nginxClient "github.com/nginxinc/nginx-plus-go-client/v2/client" + nginxClient "github.com/nginx/nginx-plus-go-client/v2/client" ) -// NginxClientInterface defines the functions used on the NGINX Plus client, abstracting away the full details of that client. +var _ NginxClientInterface = (*nginxClient.NginxClient)(nil) + +// NginxClientInterface defines the functions used on the NGINX Plus client, +// abstracting away the full details of that client. type NginxClientInterface interface { // DeleteStreamServer is used by the NginxStreamBorderClient. DeleteStreamServer(ctx context.Context, upstream string, server string) error // UpdateStreamServers is used by the NginxStreamBorderClient. - UpdateStreamServers(ctx context.Context, upstream string, servers []nginxClient.StreamUpstreamServer) ([]nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, error) + UpdateStreamServers( + ctx context.Context, + upstream string, + servers []nginxClient.StreamUpstreamServer, + ) ([]nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, error) - // DeleteHTTPServer is used by the NginxHttpBorderClient. + // DeleteHTTPServer is used by the NginxHTTPBorderClient. DeleteHTTPServer(ctx context.Context, upstream string, server string) error - // UpdateHTTPServers is used by the NginxHttpBorderClient. - UpdateHTTPServers(ctx context.Context, upstream string, servers []nginxClient.UpstreamServer) ([]nginxClient.UpstreamServer, []nginxClient.UpstreamServer, []nginxClient.UpstreamServer, error) + // UpdateHTTPServers is used by the NginxHTTPBorderClient. + UpdateHTTPServers( + ctx context.Context, + upstream string, + servers []nginxClient.UpstreamServer, + ) ([]nginxClient.UpstreamServer, []nginxClient.UpstreamServer, []nginxClient.UpstreamServer, error) } diff --git a/internal/application/nginx_http_border_client.go b/internal/application/nginx_http_border_client.go index 2d26c14..4de147e 100644 --- a/internal/application/nginx_http_border_client.go +++ b/internal/application/nginx_http_border_client.go @@ -2,41 +2,40 @@ * Copyright 2023 F5 Inc. All rights reserved. * Use of this source code is governed by the Apache License that can be found in the LICENSE file. */ - +// dupl complains about duplicates with nginx_stream_border_client.go +//nolint:dupl package application import ( "context" "fmt" + nginxClient "github.com/nginx/nginx-plus-go-client/v2/client" "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - nginxClient "github.com/nginxinc/nginx-plus-go-client/v2/client" ) // NginxHttpBorderClient implements the BorderClient interface for HTTP upstreams. -type NginxHttpBorderClient struct { +type NginxHTTPBorderClient struct { BorderClient nginxClient NginxClientInterface - ctx context.Context } -// NewNginxHttpBorderClient is the Factory function for creating an NginxHttpBorderClient. -func NewNginxHttpBorderClient(client interface{}) (Interface, error) { +// NewNginxHTTPBorderClient is the Factory function for creating an NewNginxHTTPBorderClient. +func NewNginxHTTPBorderClient(client interface{}) (Interface, error) { ngxClient, ok := client.(NginxClientInterface) if !ok { return nil, fmt.Errorf(`expected a NginxClientInterface, got a %v`, client) } - return &NginxHttpBorderClient{ + return &NginxHTTPBorderClient{ nginxClient: ngxClient, - ctx: context.Background(), }, nil } // Update manages the Upstream servers for the Upstream Name given in the ServerUpdateEvent. -func (hbc *NginxHttpBorderClient) Update(event *core.ServerUpdateEvent) error { - httpUpstreamServers := asNginxHttpUpstreamServers(event.UpstreamServers) - _, _, _, err := hbc.nginxClient.UpdateHTTPServers(hbc.ctx, event.UpstreamName, httpUpstreamServers) +func (hbc *NginxHTTPBorderClient) Update(ctx context.Context, event *core.ServerUpdateEvent) error { + httpUpstreamServers := asNginxHTTPUpstreamServers(event.UpstreamServers) + _, _, _, err := hbc.nginxClient.UpdateHTTPServers(ctx, event.UpstreamName, httpUpstreamServers) if err != nil { return fmt.Errorf(`error occurred updating the nginx+ upstream server: %w`, err) } @@ -45,8 +44,8 @@ func (hbc *NginxHttpBorderClient) Update(event *core.ServerUpdateEvent) error { } // Delete deletes the Upstream server for the Upstream Name given in the ServerUpdateEvent. -func (hbc *NginxHttpBorderClient) Delete(event *core.ServerUpdateEvent) error { - err := hbc.nginxClient.DeleteHTTPServer(hbc.ctx, event.UpstreamName, event.UpstreamServers[0].Host) +func (hbc *NginxHTTPBorderClient) Delete(ctx context.Context, event *core.ServerUpdateEvent) error { + err := hbc.nginxClient.DeleteHTTPServer(ctx, event.UpstreamName, event.UpstreamServers[0].Host) if err != nil { return fmt.Errorf(`error occurred deleting the nginx+ upstream server: %w`, err) } @@ -55,18 +54,18 @@ func (hbc *NginxHttpBorderClient) Delete(event *core.ServerUpdateEvent) error { } // asNginxHttpUpstreamServer converts a core.UpstreamServer to a nginxClient.UpstreamServer. -func asNginxHttpUpstreamServer(server *core.UpstreamServer) nginxClient.UpstreamServer { +func asNginxHTTPUpstreamServer(server *core.UpstreamServer) nginxClient.UpstreamServer { return nginxClient.UpstreamServer{ Server: server.Host, } } -// asNginxHttpUpstreamServers converts a core.UpstreamServers to a []nginxClient.UpstreamServer. -func asNginxHttpUpstreamServers(servers core.UpstreamServers) []nginxClient.UpstreamServer { - var upstreamServers []nginxClient.UpstreamServer +// asNginxHTTPUpstreamServers converts a core.UpstreamServers to a []nginxClient.UpstreamServer. +func asNginxHTTPUpstreamServers(servers core.UpstreamServers) []nginxClient.UpstreamServer { + upstreamServers := []nginxClient.UpstreamServer{} for _, server := range servers { - upstreamServers = append(upstreamServers, asNginxHttpUpstreamServer(server)) + upstreamServers = append(upstreamServers, asNginxHTTPUpstreamServer(server)) } return upstreamServers diff --git a/internal/application/nginx_http_border_client_test.go b/internal/application/nginx_http_border_client_test.go index defc2ef..7a97206 100644 --- a/internal/application/nginx_http_border_client_test.go +++ b/internal/application/nginx_http_border_client_test.go @@ -3,20 +3,25 @@ * Use of this source code is governed by the Apache License that can be found in the LICENSE file. */ +// dupl complains about duplicates with nginx_stream_border_client_test.go +// +//nolint:dupl package application import ( + "context" "testing" ) func TestHttpBorderClient_Delete(t *testing.T) { - event := buildServerUpdateEvent(deletedEventType, ClientTypeNginxHttp) - borderClient, nginxClient, err := buildBorderClient(ClientTypeNginxHttp) + t.Parallel() + event := buildServerUpdateEvent(deletedEventType, ClientTypeNginxHTTP) + borderClient, nginxClient, err := buildBorderClient(ClientTypeNginxHTTP) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Delete(event) + err = borderClient.Delete(context.Background(), event) if err != nil { t.Fatalf(`error occurred deleting the nginx+ upstream server: %v`, err) } @@ -27,13 +32,14 @@ func TestHttpBorderClient_Delete(t *testing.T) { } func TestHttpBorderClient_Update(t *testing.T) { - event := buildServerUpdateEvent(createEventType, ClientTypeNginxHttp) - borderClient, nginxClient, err := buildBorderClient(ClientTypeNginxHttp) + t.Parallel() + event := buildServerUpdateEvent(createEventType, ClientTypeNginxHTTP) + borderClient, nginxClient, err := buildBorderClient(ClientTypeNginxHTTP) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Update(event) + err = borderClient.Update(context.Background(), event) if err != nil { t.Fatalf(`error occurred deleting the nginx+ upstream server: %v`, err) } @@ -44,21 +50,23 @@ func TestHttpBorderClient_Update(t *testing.T) { } func TestHttpBorderClient_BadNginxClient(t *testing.T) { + t.Parallel() var emptyInterface interface{} - _, err := NewBorderClient(ClientTypeNginxHttp, emptyInterface) + _, err := NewBorderClient(ClientTypeNginxHTTP, emptyInterface) if err == nil { t.Fatalf(`expected an error to occur when creating a new border client`) } } func TestHttpBorderClient_DeleteReturnsError(t *testing.T) { - event := buildServerUpdateEvent(deletedEventType, ClientTypeNginxHttp) - borderClient, _, err := buildTerrorizingBorderClient(ClientTypeNginxHttp) + t.Parallel() + event := buildServerUpdateEvent(deletedEventType, ClientTypeNginxHTTP) + borderClient, err := buildTerrorizingBorderClient(ClientTypeNginxHTTP) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Delete(event) + err = borderClient.Delete(context.Background(), event) if err == nil { t.Fatalf(`expected an error to occur when deleting the nginx+ upstream server`) @@ -66,13 +74,14 @@ func TestHttpBorderClient_DeleteReturnsError(t *testing.T) { } func TestHttpBorderClient_UpdateReturnsError(t *testing.T) { - event := buildServerUpdateEvent(createEventType, ClientTypeNginxHttp) - borderClient, _, err := buildTerrorizingBorderClient(ClientTypeNginxHttp) + t.Parallel() + event := buildServerUpdateEvent(createEventType, ClientTypeNginxHTTP) + borderClient, err := buildTerrorizingBorderClient(ClientTypeNginxHTTP) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Update(event) + err = borderClient.Update(context.Background(), event) if err == nil { t.Fatalf(`expected an error to occur when deleting the nginx+ upstream server`) diff --git a/internal/application/nginx_stream_border_client.go b/internal/application/nginx_stream_border_client.go index 6c090eb..238a22b 100644 --- a/internal/application/nginx_stream_border_client.go +++ b/internal/application/nginx_stream_border_client.go @@ -2,22 +2,22 @@ * Copyright 2023 F5 Inc. All rights reserved. * Use of this source code is governed by the Apache License that can be found in the LICENSE file. */ - +// dupl complains about duplicates with nginx_http_border_client.go +//nolint:dupl package application import ( "context" "fmt" + nginxClient "github.com/nginx/nginx-plus-go-client/v2/client" "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - nginxClient "github.com/nginxinc/nginx-plus-go-client/v2/client" ) // NginxStreamBorderClient implements the BorderClient interface for stream upstreams. type NginxStreamBorderClient struct { BorderClient nginxClient NginxClientInterface - ctx context.Context } // NewNginxStreamBorderClient is the Factory function for creating an NginxStreamBorderClient. @@ -29,14 +29,13 @@ func NewNginxStreamBorderClient(client interface{}) (Interface, error) { return &NginxStreamBorderClient{ nginxClient: ngxClient, - ctx: context.Background(), }, nil } // Update manages the Upstream servers for the Upstream Name given in the ServerUpdateEvent. -func (tbc *NginxStreamBorderClient) Update(event *core.ServerUpdateEvent) error { +func (tbc *NginxStreamBorderClient) Update(ctx context.Context, event *core.ServerUpdateEvent) error { streamUpstreamServers := asNginxStreamUpstreamServers(event.UpstreamServers) - _, _, _, err := tbc.nginxClient.UpdateStreamServers(tbc.ctx, event.UpstreamName, streamUpstreamServers) + _, _, _, err := tbc.nginxClient.UpdateStreamServers(ctx, event.UpstreamName, streamUpstreamServers) if err != nil { return fmt.Errorf(`error occurred updating the nginx+ upstream server: %w`, err) } @@ -45,8 +44,8 @@ func (tbc *NginxStreamBorderClient) Update(event *core.ServerUpdateEvent) error } // Delete deletes the Upstream server for the Upstream Name given in the ServerUpdateEvent. -func (tbc *NginxStreamBorderClient) Delete(event *core.ServerUpdateEvent) error { - err := tbc.nginxClient.DeleteStreamServer(tbc.ctx, event.UpstreamName, event.UpstreamServers[0].Host) +func (tbc *NginxStreamBorderClient) Delete(ctx context.Context, event *core.ServerUpdateEvent) error { + err := tbc.nginxClient.DeleteStreamServer(ctx, event.UpstreamName, event.UpstreamServers[0].Host) if err != nil { return fmt.Errorf(`error occurred deleting the nginx+ upstream server: %w`, err) } @@ -61,7 +60,7 @@ func asNginxStreamUpstreamServer(server *core.UpstreamServer) nginxClient.Stream } func asNginxStreamUpstreamServers(servers core.UpstreamServers) []nginxClient.StreamUpstreamServer { - var upstreamServers []nginxClient.StreamUpstreamServer + upstreamServers := []nginxClient.StreamUpstreamServer{} for _, server := range servers { upstreamServers = append(upstreamServers, asNginxStreamUpstreamServer(server)) diff --git a/internal/application/nginx_stream_border_client_test.go b/internal/application/nginx_stream_border_client_test.go index ddcb346..cf4d302 100644 --- a/internal/application/nginx_stream_border_client_test.go +++ b/internal/application/nginx_stream_border_client_test.go @@ -2,21 +2,24 @@ * Copyright 2023 F5 Inc. All rights reserved. * Use of this source code is governed by the Apache License that can be found in the LICENSE file. */ - +// dupl complains about duplicates with nginx_http_border_client_test.go +//nolint:dupl package application import ( + "context" "testing" ) func TestTcpBorderClient_Delete(t *testing.T) { + t.Parallel() event := buildServerUpdateEvent(deletedEventType, ClientTypeNginxStream) borderClient, nginxClient, err := buildBorderClient(ClientTypeNginxStream) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Delete(event) + err = borderClient.Delete(context.Background(), event) if err != nil { t.Fatalf(`error occurred deleting the nginx+ upstream server: %v`, err) } @@ -27,13 +30,14 @@ func TestTcpBorderClient_Delete(t *testing.T) { } func TestTcpBorderClient_Update(t *testing.T) { + t.Parallel() event := buildServerUpdateEvent(createEventType, ClientTypeNginxStream) borderClient, nginxClient, err := buildBorderClient(ClientTypeNginxStream) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Update(event) + err = borderClient.Update(context.Background(), event) if err != nil { t.Fatalf(`error occurred deleting the nginx+ upstream server: %v`, err) } @@ -44,6 +48,7 @@ func TestTcpBorderClient_Update(t *testing.T) { } func TestTcpBorderClient_BadNginxClient(t *testing.T) { + t.Parallel() var emptyInterface interface{} _, err := NewBorderClient(ClientTypeNginxStream, emptyInterface) if err == nil { @@ -52,13 +57,14 @@ func TestTcpBorderClient_BadNginxClient(t *testing.T) { } func TestTcpBorderClient_DeleteReturnsError(t *testing.T) { + t.Parallel() event := buildServerUpdateEvent(deletedEventType, ClientTypeNginxStream) - borderClient, _, err := buildTerrorizingBorderClient(ClientTypeNginxStream) + borderClient, err := buildTerrorizingBorderClient(ClientTypeNginxStream) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Delete(event) + err = borderClient.Delete(context.Background(), event) if err == nil { t.Fatalf(`expected an error to occur when deleting the nginx+ upstream server`) @@ -66,13 +72,14 @@ func TestTcpBorderClient_DeleteReturnsError(t *testing.T) { } func TestTcpBorderClient_UpdateReturnsError(t *testing.T) { + t.Parallel() event := buildServerUpdateEvent(createEventType, ClientTypeNginxStream) - borderClient, _, err := buildTerrorizingBorderClient(ClientTypeNginxStream) + borderClient, err := buildTerrorizingBorderClient(ClientTypeNginxStream) if err != nil { t.Fatalf(`error occurred creating a new border client: %v`, err) } - err = borderClient.Update(event) + err = borderClient.Update(context.Background(), event) if err == nil { t.Fatalf(`expected an error to occur when deleting the nginx+ upstream server`) diff --git a/internal/application/null_border_client.go b/internal/application/null_border_client.go index 8370fe0..dc4467d 100644 --- a/internal/application/null_border_client.go +++ b/internal/application/null_border_client.go @@ -6,14 +6,16 @@ package application import ( + "context" + "log/slog" + "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - "github.com/sirupsen/logrus" ) // NullBorderClient is a BorderClient that does nothing. -// / It serves only to prevent a panic if the BorderClient is not set correctly and errors from the factory methods are ignored. -type NullBorderClient struct { -} +// It serves only to prevent a panic if the BorderClient +// is not set correctly and errors from the factory methods are ignored. +type NullBorderClient struct{} // NewNullBorderClient is the Factory function for creating a NullBorderClient func NewNullBorderClient() (Interface, error) { @@ -21,13 +23,13 @@ func NewNullBorderClient() (Interface, error) { } // Update logs a Warning. It is, after all, a NullObject Pattern implementation. -func (nbc *NullBorderClient) Update(_ *core.ServerUpdateEvent) error { - logrus.Warn("NullBorderClient.Update called") +func (nbc *NullBorderClient) Update(_ context.Context, _ *core.ServerUpdateEvent) error { + slog.Warn("NullBorderClient.Update called") return nil } // Delete logs a Warning. It is, after all, a NullObject Pattern implementation. -func (nbc *NullBorderClient) Delete(_ *core.ServerUpdateEvent) error { - logrus.Warn("NullBorderClient.Delete called") +func (nbc *NullBorderClient) Delete(_ context.Context, _ *core.ServerUpdateEvent) error { + slog.Warn("NullBorderClient.Delete called") return nil } diff --git a/internal/application/null_border_client_test.go b/internal/application/null_border_client_test.go index 42e9dfb..01d9fe2 100644 --- a/internal/application/null_border_client_test.go +++ b/internal/application/null_border_client_test.go @@ -5,19 +5,24 @@ package application -import "testing" +import ( + "context" + "testing" +) func TestNullBorderClient_Delete(t *testing.T) { + t.Parallel() client := NullBorderClient{} - err := client.Delete(nil) + err := client.Delete(context.Background(), nil) if err != nil { t.Errorf(`expected no error deleting border client, got: %v`, err) } } func TestNullBorderClient_Update(t *testing.T) { + t.Parallel() client := NullBorderClient{} - err := client.Update(nil) + err := client.Update(context.Background(), nil) if err != nil { t.Errorf(`expected no error updating border client, got: %v`, err) } diff --git a/internal/authentication/doc.go b/internal/authentication/doc.go deleted file mode 100644 index 109255e..0000000 --- a/internal/authentication/doc.go +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -/* -Package authentication includes functionality to secure communications between NLK and NGINX Plus hosts. -*/ - -package authentication diff --git a/internal/authentication/factory.go b/internal/authentication/factory.go deleted file mode 100644 index 8b8d06e..0000000 --- a/internal/authentication/factory.go +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - * - * Factory for creating tls.Config objects based on the provided `tls-mode`. - */ - -package authentication - -import ( - "crypto/tls" - "crypto/x509" - "encoding/pem" - "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/certification" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/sirupsen/logrus" -) - -func NewTlsConfig(settings *configuration.Settings) (*tls.Config, error) { - logrus.Debugf("authentication::NewTlsConfig Creating TLS config for mode: '%s'", settings.TlsMode) - switch settings.TlsMode { - - case configuration.NoTLS: - return buildBasicTlsConfig(true), nil - - case configuration.SelfSignedTLS: // needs ca cert - return buildSelfSignedTlsConfig(settings.Certificates) - - case configuration.SelfSignedMutualTLS: // needs ca cert and client cert - return buildSelfSignedMtlsConfig(settings.Certificates) - - case configuration.CertificateAuthorityTLS: // needs nothing - return buildBasicTlsConfig(false), nil - - case configuration.CertificateAuthorityMutualTLS: // needs client cert - return buildCaTlsConfig(settings.Certificates) - - default: - return nil, fmt.Errorf("unknown TLS mode: %s", settings.TlsMode) - } -} - -func buildSelfSignedTlsConfig(certificates *certification.Certificates) (*tls.Config, error) { - logrus.Debug("authentication::buildSelfSignedTlsConfig Building self-signed TLS config") - certPool, err := buildCaCertificatePool(certificates.GetCACertificate()) - if err != nil { - return nil, err - } - - return &tls.Config{ - InsecureSkipVerify: false, - RootCAs: certPool, - }, nil -} - -func buildSelfSignedMtlsConfig(certificates *certification.Certificates) (*tls.Config, error) { - logrus.Debug("authentication::buildSelfSignedMtlsConfig Building self-signed mTLS config") - certPool, err := buildCaCertificatePool(certificates.GetCACertificate()) - if err != nil { - return nil, err - } - - certificate, err := buildCertificates(certificates.GetClientCertificate()) - if err != nil { - return nil, err - } - logrus.Debugf("buildSelfSignedMtlsConfig Certificate: %v", certificate) - - return &tls.Config{ - InsecureSkipVerify: false, - RootCAs: certPool, - ClientAuth: tls.RequireAndVerifyClientCert, - Certificates: []tls.Certificate{certificate}, - }, nil -} - -func buildBasicTlsConfig(skipVerify bool) *tls.Config { - logrus.Debugf("authentication::buildBasicTlsConfig skipVerify(%v)", skipVerify) - return &tls.Config{ - InsecureSkipVerify: skipVerify, - } -} - -func buildCaTlsConfig(certificates *certification.Certificates) (*tls.Config, error) { - logrus.Debug("authentication::buildCaTlsConfig") - certificate, err := buildCertificates(certificates.GetClientCertificate()) - if err != nil { - return nil, err - } - - return &tls.Config{ - InsecureSkipVerify: false, - Certificates: []tls.Certificate{certificate}, - }, nil -} - -func buildCertificates(privateKeyPEM []byte, certificatePEM []byte) (tls.Certificate, error) { - logrus.Debug("authentication::buildCertificates") - return tls.X509KeyPair(certificatePEM, privateKeyPEM) -} - -func buildCaCertificatePool(caCert []byte) (*x509.CertPool, error) { - logrus.Debug("authentication::buildCaCertificatePool") - block, _ := pem.Decode(caCert) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block containing CA certificate") - } - - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("error parsing certificate: %w", err) - } - - caCertPool := x509.NewCertPool() - caCertPool.AddCert(cert) - - return caCertPool, nil -} diff --git a/internal/authentication/factory_test.go b/internal/authentication/factory_test.go deleted file mode 100644 index a535200..0000000 --- a/internal/authentication/factory_test.go +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package authentication - -import ( - "testing" - - "github.com/nginxinc/kubernetes-nginx-ingress/internal/certification" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" -) - -const ( - CaCertificateSecretKey = "nlk-tls-ca-secret" - ClientCertificateSecretKey = "nlk-tls-client-secret" -) - -func TestTlsFactory_UnspecifiedModeDefaultsToNoTls(t *testing.T) { - settings := configuration.Settings{} - - tlsConfig, err := NewTlsConfig(&settings) - if err != nil { - t.Fatalf(`Unexpected error: %v`, err) - } - - if tlsConfig == nil { - t.Fatalf(`tlsConfig should not be nil`) - } - - if tlsConfig.InsecureSkipVerify != true { - t.Fatalf(`tlsConfig.InsecureSkipVerify should be true`) - } -} - -func TestTlsFactory_SelfSignedTlsMode(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(caCertificatePEM()) - - settings := configuration.Settings{ - TlsMode: configuration.SelfSignedTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - CaCertificateSecretKey: CaCertificateSecretKey, - ClientCertificateSecretKey: ClientCertificateSecretKey, - }, - } - - tlsConfig, err := NewTlsConfig(&settings) - if err != nil { - t.Fatalf(`Unexpected error: %v`, err) - } - - if tlsConfig == nil { - t.Fatalf(`tlsConfig should not be nil`) - } - - if tlsConfig.InsecureSkipVerify != false { - t.Fatalf(`tlsConfig.InsecureSkipVerify should be false`) - } - - if len(tlsConfig.Certificates) != 0 { - t.Fatalf(`tlsConfig.Certificates should be empty`) - } - - if tlsConfig.RootCAs == nil { - t.Fatalf(`tlsConfig.RootCAs should not be nil`) - } -} - -func TestTlsFactory_SelfSignedTlsModeCertPoolError(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(invalidCertificatePEM()) - - settings := configuration.Settings{ - TlsMode: configuration.SelfSignedTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - }, - } - - _, err := NewTlsConfig(&settings) - if err == nil { - t.Fatalf(`Expected an error`) - } - - if err.Error() != "failed to decode PEM block containing CA certificate" { - t.Fatalf(`Unexpected error message: %v`, err) - } -} - -func TestTlsFactory_SelfSignedTlsModeCertPoolCertificateParseError(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(invalidCertificateDataPEM()) - - settings := configuration.Settings{ - TlsMode: configuration.SelfSignedTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - CaCertificateSecretKey: CaCertificateSecretKey, - ClientCertificateSecretKey: ClientCertificateSecretKey, - }, - } - - _, err := NewTlsConfig(&settings) - if err == nil { - t.Fatalf(`Expected an error`) - } - - if err.Error() != "error parsing certificate: x509: inner and outer signature algorithm identifiers don't match" { - t.Fatalf(`Unexpected error message: %v`, err) - } -} - -func TestTlsFactory_SelfSignedMtlsMode(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(caCertificatePEM()) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), clientCertificatePEM()) - - settings := configuration.Settings{ - TlsMode: configuration.SelfSignedMutualTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - CaCertificateSecretKey: CaCertificateSecretKey, - ClientCertificateSecretKey: ClientCertificateSecretKey, - }, - } - - tlsConfig, err := NewTlsConfig(&settings) - if err != nil { - t.Fatalf(`Unexpected error: %v`, err) - } - - if tlsConfig == nil { - t.Fatalf(`tlsConfig should not be nil`) - } - - if tlsConfig.InsecureSkipVerify != false { - t.Fatalf(`tlsConfig.InsecureSkipVerify should be false`) - } - - if len(tlsConfig.Certificates) == 0 { - t.Fatalf(`tlsConfig.Certificates should not be empty`) - } - - if tlsConfig.RootCAs == nil { - t.Fatalf(`tlsConfig.RootCAs should not be nil`) - } -} - -func TestTlsFactory_SelfSignedMtlsModeCertPoolError(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(invalidCertificatePEM()) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), clientCertificatePEM()) - - settings := configuration.Settings{ - TlsMode: configuration.SelfSignedMutualTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - }, - } - - _, err := NewTlsConfig(&settings) - if err == nil { - t.Fatalf(`Expected an error`) - } - - if err.Error() != "failed to decode PEM block containing CA certificate" { - t.Fatalf(`Unexpected error message: %v`, err) - } -} - -func TestTlsFactory_SelfSignedMtlsModeClientCertificateError(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(caCertificatePEM()) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), invalidCertificatePEM()) - - settings := configuration.Settings{ - TlsMode: configuration.SelfSignedMutualTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - CaCertificateSecretKey: CaCertificateSecretKey, - ClientCertificateSecretKey: ClientCertificateSecretKey, - }, - } - - _, err := NewTlsConfig(&settings) - if err == nil { - t.Fatalf(`Expected an error`) - } - - if err.Error() != "tls: failed to find any PEM data in certificate input" { - t.Fatalf(`Unexpected error message: %v`, err) - } -} - -func TestTlsFactory_CaTlsMode(t *testing.T) { - settings := configuration.Settings{ - TlsMode: configuration.CertificateAuthorityTLS, - } - - tlsConfig, err := NewTlsConfig(&settings) - if err != nil { - t.Fatalf(`Unexpected error: %v`, err) - } - - if tlsConfig == nil { - t.Fatalf(`tlsConfig should not be nil`) - } - - if tlsConfig.InsecureSkipVerify != false { - t.Fatalf(`tlsConfig.InsecureSkipVerify should be false`) - } - - if len(tlsConfig.Certificates) != 0 { - t.Fatalf(`tlsConfig.Certificates should be empty`) - } - - if tlsConfig.RootCAs != nil { - t.Fatalf(`tlsConfig.RootCAs should be nil`) - } -} - -func TestTlsFactory_CaMtlsMode(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), clientCertificatePEM()) - - settings := configuration.Settings{ - TlsMode: configuration.CertificateAuthorityMutualTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - CaCertificateSecretKey: CaCertificateSecretKey, - ClientCertificateSecretKey: ClientCertificateSecretKey, - }, - } - - tlsConfig, err := NewTlsConfig(&settings) - if err != nil { - t.Fatalf(`Unexpected error: %v`, err) - } - - if tlsConfig == nil { - t.Fatalf(`tlsConfig should not be nil`) - } - - if tlsConfig.InsecureSkipVerify != false { - t.Fatalf(`tlsConfig.InsecureSkipVerify should be false`) - } - - if len(tlsConfig.Certificates) == 0 { - t.Fatalf(`tlsConfig.Certificates should not be empty`) - } - - if tlsConfig.RootCAs != nil { - t.Fatalf(`tlsConfig.RootCAs should be nil`) - } -} - -func TestTlsFactory_CaMtlsModeClientCertificateError(t *testing.T) { - certificates := make(map[string]map[string]core.SecretBytes) - certificates[CaCertificateSecretKey] = buildCaCertificateEntry(caCertificatePEM()) - certificates[ClientCertificateSecretKey] = buildClientCertificateEntry(clientKeyPEM(), invalidCertificatePEM()) - - settings := configuration.Settings{ - TlsMode: configuration.CertificateAuthorityMutualTLS, - Certificates: &certification.Certificates{ - Certificates: certificates, - }, - } - - _, err := NewTlsConfig(&settings) - if err == nil { - t.Fatalf(`Expected an error`) - } - - if err.Error() != "tls: failed to find any PEM data in certificate input" { - t.Fatalf(`Unexpected error message: %v`, err) - } -} - -// caCertificatePEM returns a PEM-encoded CA certificate. -// Note: The certificate is self-signed and generated explicitly for tests, -// it is not used anywhere else. -func caCertificatePEM() string { - return ` ------BEGIN CERTIFICATE----- -MIIDTzCCAjcCFA4Zdj3E9TdjOP48eBRDGRLfkj7CMA0GCSqGSIb3DQEBCwUAMGQx -CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0 -dGxlMQ4wDAYDVQQKDAVOR0lOWDEeMBwGA1UECwwVQ29tbXVuaXR5ICYgQWxsaWFu -Y2VzMB4XDTIzMDkyOTE3MTY1MVoXDTIzMTAyOTE3MTY1MVowZDELMAkGA1UEBhMC -VVMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxDjAMBgNV -BAoMBU5HSU5YMR4wHAYDVQQLDBVDb21tdW5pdHkgJiBBbGxpYW5jZXMwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwlI4ZvJ/6hvqULFVL+1ZSRDTPQ48P -umehJhPz6xPhC9UkeTe2FZxm2Rsi1I5QXm/bTG2OcX775jgXzae9NQjctxwrz4Ks -LOWUvRkkfhQR67xk0Noux76/9GWGnB+Fapn54tlWql6uHQfOu1y7MCRkZ27zHbkk -lq4Oa2RmX8rIyECWgbTyL0kETBVJU8bYORQ5JjhRlz08inq3PggY8blrehIetrWN -dw+gzcqdvAI2uSCodHTHM/77KipnYmPiSiDjSDRlXdxTG8JnyIB78IoH/sw6RyBm -CvVa3ytvKziXAvbBoXq5On5WmMRF97p/MmBc53ExMuDZjA4fisnViS0PAgMBAAEw -DQYJKoZIhvcNAQELBQADggEBAJeoa2P59zopLjBInx/DnWn1N1CmFLb0ejKxG2jh -cOw15Sx40O0XrtrAto38iu4R/bkBeNCSUILlT+A3uYDila92Dayvls58WyIT3meD -G6+Sx/QDF69+4AXpVy9mQ+hxcofpFA32+GOMXwmk2OrAcdSkkGSBhZXgvTpQ64dl -xSiQ5EQW/K8LoBoEOXfjIZJNPORgKn5MI09AY7/47ycKDKTUU2yO8AtIHYKttw0x -kfIg7QOdo1F9IXVpGjJI7ynyrgsCEYxMoDyH42Dq84eKgrUFLEXemEz8hgdFgK41 -0eUYhAtzWHbRPBp+U/34CQoZ5ChNFp2YipvtXrzKE8KLkuM= ------END CERTIFICATE----- -` -} - -func invalidCertificatePEM() string { - return ` ------BEGIN CERTIFICATE----- -MIIClzCCAX+gAwIBAgIJAIfPhC0RG6CwMA0GCSqGSIb3DQEBCwUAMBkxFzAVBgNV -BAMMDm9pbCBhdXRob3JpdHkwHhcNMjAwNDA3MTUwOTU1WhcNMjEwNDA2MTUwOTU1 -WjBMMSAwHgYDVQQLDBd5b3VuZy1jaGFsbGVuZ2UgdGVzdCBjb25zdW1lczEfMB0G -A1UECwwWc28wMS5jb3Jwb3JhdGlvbnNvY2lhbDEhMB8GA1UEAwwYc29tMS5jb3Jw -b3JhdGlvbnNvY2lhbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQDGRX31uzy+yLUOz7wOJHHm2dzrDgUbC6RZDjURvZxyt2Zi5wYWsEB5r5YhN7L0 -y1R9f+MGwNITIz9nYZuU/PLFOvzF5qX7A8TbdgjZEqvXe2NZ9J2z3iWvYQLN8Py3 -nv/Y6wadgXEBRCNNuIg/bQ9XuOr9tfB6j4Ut1GLU0eIlV/L3Rf9Y6SgrAl+58ITj -Wrg3Js/Wz3J2JU4qBD8U4I3XvUyfnX2SAG8Llm4KBuYz7g63Iu05s6RnmG+Xhu2T -5f2DWZUeATWbAlUW/M4NLO1+5H0gOr0TGulETQ6uElMchT7s/H6Rv1CV+CNCCgEI -adRjWJq9yQ+KrE+urSMCXu8XAgMBAAGjUzBRMB0GA1UdDgQWBBRb40pKGU4lNvqB -1f5Mz3t0N/K3hzAfBgNVHSMEGDAWgBRb40pKGU4lNvqB1f5Mz3t0N/K3hzAPBgNV -HREECDAGhwQAAAAAAAAwCgYIKoZIzj0EAwIDSAAwRQIhAP3ST/mXyRXsU2ciRoE -gE6trllODFY+9FgT6UbF2TwzAiAAuaUxtbk6uXLqtD5NtXqOQf0Ckg8GQxc5V1G2 -9PqTXQ== ------END CERTIFICATE----- -` -} - -// Yoinked from https://cs.opensource.google/go/go/+/refs/tags/go1.21.1:src/crypto/x509/x509_test.go, line 3385 -// This allows the `buildCaCertificatePool(...)` --> `x509.ParseCertificate(...)` call error branch to be covered. -func invalidCertificateDataPEM() string { - return ` ------BEGIN CERTIFICATE----- -MIIBBzCBrqADAgECAgEAMAoGCCqGSM49BAMCMAAwIhgPMDAwMTAxMDEwMDAwMDBa -GA8wMDAxMDEwMTAwMDAwMFowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABOqV -EDuVXxwZgIU3+dOwv1SsMu0xuV48hf7xmK8n7sAMYgllB+96DnPqBeboJj4snYnx -0AcE0PDVQ1l4Z3YXsQWjFTATMBEGA1UdEQEB/wQHMAWCA2FzZDAKBggqhkjOPQQD -AwNIADBFAiBi1jz/T2HT5nAfrD7zsgR+68qh7Erc6Q4qlxYBOgKG4QIhAOtjIn+Q -tA+bq+55P3ntxTOVRq0nv1mwnkjwt9cQR9Fn ------END CERTIFICATE----- -` -} - -// clientCertificatePEM returns a PEM-encoded client certificate. -// Note: The certificate is self-signed and generated explicitly for tests, -// it is not used anywhere else. -func clientCertificatePEM() string { - return ` ------BEGIN CERTIFICATE----- -MIIEDDCCAvSgAwIBAgIULDFXwGrTohN/PRao2rSLk9VxFdgwDQYJKoZIhvcNAQEL -BQAwXTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcM -CUluZGlhbm9sYTEPMA0GA1UECgwGV2FnbmVyMRQwEgYDVQQLDAtEZXZlbG9wbWVu -dDAeFw0yMzA5MjkxNzA3NTRaFw0yNDA5MjgxNzA3NTRaMGQxCzAJBgNVBAYTAlVT -MRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMQ4wDAYDVQQK -DAVOR0lOWDEeMBwGA1UECwwVQ29tbXVuaXR5ICYgQWxsaWFuY2VzMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqNuEZ6+TcFrmzcwp8u8mzk0jPd47GKk -H9wwdkFCzGdd8KJkFQhzLyimZIWkRDYmhaxZd76jKGBpdfyivR4e4Mi5WYlpPGMI -ppM7/rMYP8yn04tkokAazbqjOTlF8NUKqGQwqAN4Z/PvoG2HyP9omGpuLWTbjKto -oGr5aPBIhzlICU3OjHn6eKaekJeAYBo3uQFYOxCjtE9hJLDOY4q7zomMJfYoeoA2 -Afwkx1Lmozp2j/esB52/HlCKVhAOzZsPzM+E9eb1Q722dUed4OuiVYSfrDzeImrA -TufzTBTMEpFHCtdBGocZ3LRd9qmcP36ZCMsJNbYnQZV3XsI4JhjjHwIDAQABo4G8 -MIG5MBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBRDl4jeiE1mJDPrYmQx -g2ndkWxpYjCBggYDVR0jBHsweaFhpF8wXTELMAkGA1UEBhMCVVMxEzARBgNVBAgM -Cldhc2hpbmd0b24xEjAQBgNVBAcMCUluZGlhbm9sYTEPMA0GA1UECgwGV2FnbmVy -MRQwEgYDVQQLDAtEZXZlbG9wbWVudIIUNxx2Mr+PKXiF3d2i51fb/rnWbBgwDQYJ -KoZIhvcNAQELBQADggEBAL0wS6LkFuqGDlhaTGnAXRwRDlC6uwrm8wNWppaw9Vqt -eaZGFzodcCFp9v8jjm1LsTv7gEUBnWtn27LGP4GJSpZjiq6ulJypBxo/G0OkMByK -ky4LeGY7/BQzjzHdfXEq4gwfC45ni4n54uS9uzW3x+AwLSkxPtBxSwxhtwBLo9aE -Ql4rHUoWc81mhGO5mMZBaorxZXps1f3skfP+wZX943FIMt5gz4hkxwFp3bI/FrqH -R8DLUlCzBA9+7WIFD1wi25TV+Oyq3AjT/KiVmR+umrukhnofCWe8JiVpb5iJcd2k -Rc7+bvyb5OCnJdEX08XGWmF2/OFKLrCzLH1tQxk7VNE= ------END CERTIFICATE----- -` -} - -// clientKeyPEM returns a PEM-encoded client key. -// Note: The key is self-signed and generated explicitly for tests, -// it is not used anywhere else. -func clientKeyPEM() string { - return ` ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCio24Rnr5NwWub -NzCny7ybOTSM93jsYqQf3DB2QULMZ13womQVCHMvKKZkhaRENiaFrFl3vqMoYGl1 -/KK9Hh7gyLlZiWk8Ywimkzv+sxg/zKfTi2SiQBrNuqM5OUXw1QqoZDCoA3hn8++g -bYfI/2iYam4tZNuMq2igavlo8EiHOUgJTc6Mefp4pp6Ql4BgGje5AVg7EKO0T2Ek -sM5jirvOiYwl9ih6gDYB/CTHUuajOnaP96wHnb8eUIpWEA7Nmw/Mz4T15vVDvbZ1 -R53g66JVhJ+sPN4iasBO5/NMFMwSkUcK10EahxnctF32qZw/fpkIywk1tidBlXde -wjgmGOMfAgMBAAECggEAA+R2b2yFsHW3HhVhkDqDjpF9bPxFRB8OP4b1D/d64kp9 -CJPSYmB75T6LUO+T4WAMZvmbgI6q9/3quDyuJmmQop+bNAXiY2QZYmc2sd9Wbrx2 -rczxwSJYoeDcJDP3NQ7cPPB866B9ortHWmcUr15RgghWD7cQvBqkG+bDhlvt2HKg -NZmL6R0U1bVAlRMtFJiEdMHuGnPmoDU5IGc1fKjsgijLeMboUrEaXWINoEm8ii5e -/mnsfLCBmeJAsKuXxL8/1UmvWYE/ltDfYBVclKhcH2UWTZv7pdRtHnu49lkZivUB -ZvH2DHsSMjXj6+HHr6RcRGmnMDyfhJFPCjOdTjf4oQKBgQDeYLWZx22zGXgfb7md -MhdKed9GxMJHzs4jDouqrHy0w95vwMi7RXgeKpKXiCruqSEB/Trtq01f7ekh0mvJ -Ys0h4A5tkrT5BVVBs+65uF/kSF2z/CYGNRhAABO7UM+B1e3tlnjfjeb/M78IcFbT -FyBN90A/+a9JGZ4obt3ack3afwKBgQC7OncnXC9L5QCWForJWQCNO3q3OW1Gaoxe -OAnmnPSJ7NUd7xzDNE8pzBUWXysZCoRU3QNElcQfzHWtZx1iqJPk3ERK2awNsnV7 -X2Fu4vHzIr5ZqVnM8NG7+iWrxRLf+ctcEvPiqRYo+g+r5tTGJqWh2nh9W7iQwwwE -1ikoxFBnYQKBgCbDdOR5fwXZSrcwIorkUGsLE4Cii7s4sXYq8u2tY4+fFQcl89ex -JF8dzK/dbJ5tnPNb0Qnc8n/mWN0scN2J+3gMNnejOyitZU8urk5xdUW115+oNHig -iLmfSdE9JO7c+7yOnkNZ2QpjWsl9y6TAQ0FT+D8upv93F7q0mLebdTbBAoGBALmp -r5EThD9RlvQ+5F/oZ3imO/nH88n5TLr9/St4B7NibLAjdrVIgRwkqeCmfRl26WUy -SdRQY81YtnU/JM+59fbkSsCi/FAU4RV3ryoD2QRPNs249zkYshMjawncAuyiS/xB -OyJQpI3782B3JhZdKrDG8eb19p9vG9MMAILRsh3hAoGASCvmq10nHHGFYTerIllQ -sohNaw3KDlQTkpyOAztS4jOXwvppMXbYuCznuJbHz0NEM2ww+SiA1RTvD/gosYYC -mMgqRga/Qu3b149M3wigDjK+RAcyuNGZN98bqU/UjJLjqH6IMutt59+9XNspcD96 -z/3KkMx4uqJXZyvQrmkolSg= ------END PRIVATE KEY----- -` -} - -func buildClientCertificateEntry(keyPEM, certificatePEM string) map[string]core.SecretBytes { - return map[string]core.SecretBytes{ - certification.CertificateKey: core.SecretBytes([]byte(certificatePEM)), - certification.CertificateKeyKey: core.SecretBytes([]byte(keyPEM)), - } -} - -func buildCaCertificateEntry(certificatePEM string) map[string]core.SecretBytes { - return map[string]core.SecretBytes{ - certification.CertificateKey: core.SecretBytes([]byte(certificatePEM)), - } -} diff --git a/internal/certification/certificates.go b/internal/certification/certificates.go deleted file mode 100644 index 53bd843..0000000 --- a/internal/certification/certificates.go +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - * - * Establishes a Watcher for the Kubernetes Secrets that contain the various certificates and keys used to generate a tls.Config object; - * exposes the certificates and keys. - */ - -package certification - -import ( - "context" - "fmt" - - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" - - "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" -) - -const ( - // SecretsNamespace is the value used to filter the Secrets Resource in the Informer. - SecretsNamespace = "nlk" - - // CertificateKey is the key for the certificate in the Secret. - CertificateKey = "tls.crt" - - // CertificateKeyKey is the key for the certificate key in the Secret. - CertificateKeyKey = "tls.key" -) - -type Certificates struct { - Certificates map[string]map[string]core.SecretBytes - - // Context is the context used to control the application. - Context context.Context - - // CaCertificateSecretKey is the name of the Secret that contains the Certificate Authority certificate. - CaCertificateSecretKey string - - // ClientCertificateSecretKey is the name of the Secret that contains the Client certificate. - ClientCertificateSecretKey string - - // informer is the SharedInformer used to watch for changes to the Secrets . - informer cache.SharedInformer - - // K8sClient is the Kubernetes client used to communicate with the Kubernetes API. - k8sClient kubernetes.Interface - - // eventHandlerRegistration is the object used to track the event handlers with the SharedInformer. - eventHandlerRegistration cache.ResourceEventHandlerRegistration -} - -// NewCertificates factory method that returns a new Certificates object. -func NewCertificates(ctx context.Context, k8sClient kubernetes.Interface) *Certificates { - return &Certificates{ - k8sClient: k8sClient, - Context: ctx, - Certificates: nil, - } -} - -// GetCACertificate returns the Certificate Authority certificate. -func (c *Certificates) GetCACertificate() core.SecretBytes { - bytes := c.Certificates[c.CaCertificateSecretKey][CertificateKey] - - return bytes -} - -// GetClientCertificate returns the Client certificate and key. -func (c *Certificates) GetClientCertificate() (core.SecretBytes, core.SecretBytes) { - keyBytes := c.Certificates[c.ClientCertificateSecretKey][CertificateKeyKey] - certificateBytes := c.Certificates[c.ClientCertificateSecretKey][CertificateKey] - - return keyBytes, certificateBytes -} - -// Initialize initializes the Certificates object. Sets up a SharedInformer for the Secrets Resource. -func (c *Certificates) Initialize() error { - logrus.Info("Certificates::Initialize") - - var err error - - c.Certificates = make(map[string]map[string]core.SecretBytes) - - informer, err := c.buildInformer() - if err != nil { - return fmt.Errorf(`error occurred building an informer: %w`, err) - } - - c.informer = informer - - err = c.initializeEventHandlers() - if err != nil { - return fmt.Errorf(`error occurred initializing event handlers: %w`, err) - } - - return nil -} - -// Run starts the SharedInformer. -func (c *Certificates) Run() error { - logrus.Info("Certificates::Run") - - if c.informer == nil { - return fmt.Errorf(`initialize must be called before Run`) - } - - c.informer.Run(c.Context.Done()) - - <-c.Context.Done() - - return nil -} - -func (c *Certificates) buildInformer() (cache.SharedInformer, error) { - logrus.Debug("Certificates::buildInformer") - - options := informers.WithNamespace(SecretsNamespace) - factory := informers.NewSharedInformerFactoryWithOptions(c.k8sClient, 0, options) - informer := factory.Core().V1().Secrets().Informer() - - return informer, nil -} - -func (c *Certificates) initializeEventHandlers() error { - logrus.Debug("Certificates::initializeEventHandlers") - - var err error - - handlers := cache.ResourceEventHandlerFuncs{ - AddFunc: c.handleAddEvent, - DeleteFunc: c.handleDeleteEvent, - UpdateFunc: c.handleUpdateEvent, - } - - c.eventHandlerRegistration, err = c.informer.AddEventHandler(handlers) - if err != nil { - return fmt.Errorf(`error occurred registering event handlers: %w`, err) - } - - return nil -} - -func (c *Certificates) handleAddEvent(obj interface{}) { - logrus.Debug("Certificates::handleAddEvent") - - secret, ok := obj.(*corev1.Secret) - if !ok { - logrus.Errorf("Certificates::handleAddEvent: unable to cast object to Secret") - return - } - - c.Certificates[secret.Name] = map[string]core.SecretBytes{} - - // Input from the secret comes in the form - // tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVCVEN... - // tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0l... - // Where the keys are `tls.crt` and `tls.key` and the values are []byte - for k, v := range secret.Data { - c.Certificates[secret.Name][k] = core.SecretBytes(v) - } - - logrus.Debugf("Certificates::handleAddEvent: certificates (%d)", len(c.Certificates)) -} - -func (c *Certificates) handleDeleteEvent(obj interface{}) { - logrus.Debug("Certificates::handleDeleteEvent") - - secret, ok := obj.(*corev1.Secret) - if !ok { - logrus.Errorf("Certificates::handleDeleteEvent: unable to cast object to Secret") - return - } - - if c.Certificates[secret.Name] != nil { - delete(c.Certificates, secret.Name) - } - - logrus.Debugf("Certificates::handleDeleteEvent: certificates (%d)", len(c.Certificates)) -} - -func (c *Certificates) handleUpdateEvent(_ interface{}, newValue interface{}) { - logrus.Debug("Certificates::handleUpdateEvent") - - secret, ok := newValue.(*corev1.Secret) - if !ok { - logrus.Errorf("Certificates::handleUpdateEvent: unable to cast object to Secret") - return - } - - for k, v := range secret.Data { - c.Certificates[secret.Name][k] = v - } - - logrus.Debugf("Certificates::handleUpdateEvent: certificates (%d)", len(c.Certificates)) -} diff --git a/internal/certification/certificates_test.go b/internal/certification/certificates_test.go deleted file mode 100644 index c8edf14..0000000 --- a/internal/certification/certificates_test.go +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package certification - -import ( - "context" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" - "k8s.io/client-go/tools/cache" - "testing" - "time" -) - -const ( - CaCertificateSecretKey = "nlk-tls-ca-secret" -) - -func TestNewCertificate(t *testing.T) { - ctx := context.Background() - - certificates := NewCertificates(ctx, nil) - - if certificates == nil { - t.Fatalf(`certificates should not be nil`) - } -} - -func TestCertificates_Initialize(t *testing.T) { - certificates := NewCertificates(context.Background(), nil) - - err := certificates.Initialize() - if err != nil { - t.Fatalf(`Unexpected error: %v`, err) - } -} - -func TestCertificates_RunWithoutInitialize(t *testing.T) { - certificates := NewCertificates(context.Background(), nil) - - err := certificates.Run() - if err == nil { - t.Fatalf(`Expected error`) - } - - if err.Error() != `initialize must be called before Run` { - t.Fatalf(`Unexpected error: %v`, err) - } -} - -func TestCertificates_EmptyCertificates(t *testing.T) { - certificates := NewCertificates(context.Background(), nil) - - err := certificates.Initialize() - if err != nil { - t.Fatalf(`error Initializing Certificates: %v`, err) - } - - caBytes := certificates.GetCACertificate() - if caBytes != nil { - t.Fatalf(`Expected nil CA certificate`) - } - - clientKey, clientCert := certificates.GetClientCertificate() - if clientKey != nil { - t.Fatalf(`Expected nil client key`) - } - if clientCert != nil { - t.Fatalf(`Expected nil client certificate`) - } -} - -func TestCertificates_ExerciseHandlers(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - k8sClient := fake.NewSimpleClientset() - - certificates := NewCertificates(ctx, k8sClient) - - _ = certificates.Initialize() - - certificates.CaCertificateSecretKey = CaCertificateSecretKey - - go func() { - err := certificates.Run() - if err != nil { - t.Fatalf("error running Certificates: %v", err) - } - }() - - cache.WaitForCacheSync(ctx.Done(), certificates.informer.HasSynced) - - secret := buildSecret() - - /* -- Test Create -- */ - - created, err := k8sClient.CoreV1().Secrets(SecretsNamespace).Create(ctx, secret, metav1.CreateOptions{}) - if err != nil { - t.Fatalf(`error creating the Secret: %v`, err) - } - - if created.Name != secret.Name { - t.Fatalf(`Expected name %v, got %v`, secret.Name, created.Name) - } - - time.Sleep(2 * time.Second) - - caBytes := certificates.GetCACertificate() - if caBytes == nil { - t.Fatalf(`Expected non-nil CA certificate`) - } - - /* -- Test Update -- */ - - secret.Labels = map[string]string{"updated": "true"} - _, err = k8sClient.CoreV1().Secrets(SecretsNamespace).Update(ctx, secret, metav1.UpdateOptions{}) - if err != nil { - t.Fatalf(`error updating the Secret: %v`, err) - } - - time.Sleep(2 * time.Second) - - caBytes = certificates.GetCACertificate() - if caBytes == nil { - t.Fatalf(`Expected non-nil CA certificate`) - } - - /* -- Test Delete -- */ - - err = k8sClient.CoreV1().Secrets(SecretsNamespace).Delete(ctx, secret.Name, metav1.DeleteOptions{}) - if err != nil { - t.Fatalf(`error deleting the Secret: %v`, err) - } - - time.Sleep(2 * time.Second) - - caBytes = certificates.GetCACertificate() - if caBytes != nil { - t.Fatalf(`Expected nil CA certificate, got: %v`, caBytes) - } -} - -func buildSecret() *corev1.Secret { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: CaCertificateSecretKey, - Namespace: SecretsNamespace, - }, - Data: map[string][]byte{ - CertificateKey: []byte(certificatePEM()), - CertificateKeyKey: []byte(keyPEM()), - }, - Type: corev1.SecretTypeTLS, - } -} - -// certificatePEM returns a PEM-encoded client certificate. -// Note: The certificate is self-signed and generated explicitly for tests, -// it is not used anywhere else. -func certificatePEM() string { - return ` ------BEGIN CERTIFICATE----- -MIIEDDCCAvSgAwIBAgIULDFXwGrTohN/PRao2rSLk9VxFdgwDQYJKoZIhvcNAQEL -BQAwXTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xEjAQBgNVBAcM -CUluZGlhbm9sYTEPMA0GA1UECgwGV2FnbmVyMRQwEgYDVQQLDAtEZXZlbG9wbWVu -dDAeFw0yMzA5MjkxNzA3NTRaFw0yNDA5MjgxNzA3NTRaMGQxCzAJBgNVBAYTAlVT -MRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMQ4wDAYDVQQK -DAVOR0lOWDEeMBwGA1UECwwVQ29tbXVuaXR5ICYgQWxsaWFuY2VzMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoqNuEZ6+TcFrmzcwp8u8mzk0jPd47GKk -H9wwdkFCzGdd8KJkFQhzLyimZIWkRDYmhaxZd76jKGBpdfyivR4e4Mi5WYlpPGMI -ppM7/rMYP8yn04tkokAazbqjOTlF8NUKqGQwqAN4Z/PvoG2HyP9omGpuLWTbjKto -oGr5aPBIhzlICU3OjHn6eKaekJeAYBo3uQFYOxCjtE9hJLDOY4q7zomMJfYoeoA2 -Afwkx1Lmozp2j/esB52/HlCKVhAOzZsPzM+E9eb1Q722dUed4OuiVYSfrDzeImrA -TufzTBTMEpFHCtdBGocZ3LRd9qmcP36ZCMsJNbYnQZV3XsI4JhjjHwIDAQABo4G8 -MIG5MBMGA1UdJQQMMAoGCCsGAQUFBwMCMB0GA1UdDgQWBBRDl4jeiE1mJDPrYmQx -g2ndkWxpYjCBggYDVR0jBHsweaFhpF8wXTELMAkGA1UEBhMCVVMxEzARBgNVBAgM -Cldhc2hpbmd0b24xEjAQBgNVBAcMCUluZGlhbm9sYTEPMA0GA1UECgwGV2FnbmVy -MRQwEgYDVQQLDAtEZXZlbG9wbWVudIIUNxx2Mr+PKXiF3d2i51fb/rnWbBgwDQYJ -KoZIhvcNAQELBQADggEBAL0wS6LkFuqGDlhaTGnAXRwRDlC6uwrm8wNWppaw9Vqt -eaZGFzodcCFp9v8jjm1LsTv7gEUBnWtn27LGP4GJSpZjiq6ulJypBxo/G0OkMByK -ky4LeGY7/BQzjzHdfXEq4gwfC45ni4n54uS9uzW3x+AwLSkxPtBxSwxhtwBLo9aE -Ql4rHUoWc81mhGO5mMZBaorxZXps1f3skfP+wZX943FIMt5gz4hkxwFp3bI/FrqH -R8DLUlCzBA9+7WIFD1wi25TV+Oyq3AjT/KiVmR+umrukhnofCWe8JiVpb5iJcd2k -Rc7+bvyb5OCnJdEX08XGWmF2/OFKLrCzLH1tQxk7VNE= ------END CERTIFICATE----- -` -} - -// keyPEM returns a PEM-encoded client key. -// Note: The key is self-signed and generated explicitly for tests, -// it is not used anywhere else. -func keyPEM() string { - return ` ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCio24Rnr5NwWub -NzCny7ybOTSM93jsYqQf3DB2QULMZ13womQVCHMvKKZkhaRENiaFrFl3vqMoYGl1 -/KK9Hh7gyLlZiWk8Ywimkzv+sxg/zKfTi2SiQBrNuqM5OUXw1QqoZDCoA3hn8++g -bYfI/2iYam4tZNuMq2igavlo8EiHOUgJTc6Mefp4pp6Ql4BgGje5AVg7EKO0T2Ek -sM5jirvOiYwl9ih6gDYB/CTHUuajOnaP96wHnb8eUIpWEA7Nmw/Mz4T15vVDvbZ1 -R53g66JVhJ+sPN4iasBO5/NMFMwSkUcK10EahxnctF32qZw/fpkIywk1tidBlXde -wjgmGOMfAgMBAAECggEAA+R2b2yFsHW3HhVhkDqDjpF9bPxFRB8OP4b1D/d64kp9 -CJPSYmB75T6LUO+T4WAMZvmbgI6q9/3quDyuJmmQop+bNAXiY2QZYmc2sd9Wbrx2 -rczxwSJYoeDcJDP3NQ7cPPB866B9ortHWmcUr15RgghWD7cQvBqkG+bDhlvt2HKg -NZmL6R0U1bVAlRMtFJiEdMHuGnPmoDU5IGc1fKjsgijLeMboUrEaXWINoEm8ii5e -/mnsfLCBmeJAsKuXxL8/1UmvWYE/ltDfYBVclKhcH2UWTZv7pdRtHnu49lkZivUB -ZvH2DHsSMjXj6+HHr6RcRGmnMDyfhJFPCjOdTjf4oQKBgQDeYLWZx22zGXgfb7md -MhdKed9GxMJHzs4jDouqrHy0w95vwMi7RXgeKpKXiCruqSEB/Trtq01f7ekh0mvJ -Ys0h4A5tkrT5BVVBs+65uF/kSF2z/CYGNRhAABO7UM+B1e3tlnjfjeb/M78IcFbT -FyBN90A/+a9JGZ4obt3ack3afwKBgQC7OncnXC9L5QCWForJWQCNO3q3OW1Gaoxe -OAnmnPSJ7NUd7xzDNE8pzBUWXysZCoRU3QNElcQfzHWtZx1iqJPk3ERK2awNsnV7 -X2Fu4vHzIr5ZqVnM8NG7+iWrxRLf+ctcEvPiqRYo+g+r5tTGJqWh2nh9W7iQwwwE -1ikoxFBnYQKBgCbDdOR5fwXZSrcwIorkUGsLE4Cii7s4sXYq8u2tY4+fFQcl89ex -JF8dzK/dbJ5tnPNb0Qnc8n/mWN0scN2J+3gMNnejOyitZU8urk5xdUW115+oNHig -iLmfSdE9JO7c+7yOnkNZ2QpjWsl9y6TAQ0FT+D8upv93F7q0mLebdTbBAoGBALmp -r5EThD9RlvQ+5F/oZ3imO/nH88n5TLr9/St4B7NibLAjdrVIgRwkqeCmfRl26WUy -SdRQY81YtnU/JM+59fbkSsCi/FAU4RV3ryoD2QRPNs249zkYshMjawncAuyiS/xB -OyJQpI3782B3JhZdKrDG8eb19p9vG9MMAILRsh3hAoGASCvmq10nHHGFYTerIllQ -sohNaw3KDlQTkpyOAztS4jOXwvppMXbYuCznuJbHz0NEM2ww+SiA1RTvD/gosYYC -mMgqRga/Qu3b149M3wigDjK+RAcyuNGZN98bqU/UjJLjqH6IMutt59+9XNspcD96 -z/3KkMx4uqJXZyvQrmkolSg= ------END PRIVATE KEY----- -` -} diff --git a/internal/certification/doc.go b/internal/certification/doc.go deleted file mode 100644 index 3388ea0..0000000 --- a/internal/certification/doc.go +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -/* -Package certification includes functionality to access the Secrets containing the TLS Certificates. -*/ - -package certification diff --git a/internal/communication/factory.go b/internal/communication/factory.go index 9a3d411..2084d5c 100644 --- a/internal/communication/factory.go +++ b/internal/communication/factory.go @@ -6,21 +6,19 @@ package communication import ( - "crypto/tls" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/authentication" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/sirupsen/logrus" + "fmt" netHttp "net/http" "time" + + "github.com/nginxinc/kubernetes-nginx-ingress/pkg/buildinfo" ) -// NewHttpClient is a factory method to create a new Http Client with a default configuration. -// RoundTripper is a wrapper around the default net/communication Transport to add additional headers, in this case, -// the Headers are configured for JSON. -func NewHttpClient(settings *configuration.Settings) (*netHttp.Client, error) { - headers := NewHeaders() - tlsConfig := NewTlsConfig(settings) - transport := NewTransport(tlsConfig) +// NewHTTPClient is a factory method to create a new Http Client configured for +// working with NGINXaaS or the N+ api. If skipVerify is set to true, the http +// transport will skip TLS certificate verification. +func NewHTTPClient(apiKey string, skipVerify bool) (*netHttp.Client, error) { + headers := NewHeaders(apiKey) + transport := NewTransport(skipVerify) roundTripper := NewRoundTripper(headers, transport) return &netHttp.Client{ @@ -32,29 +30,24 @@ func NewHttpClient(settings *configuration.Settings) (*netHttp.Client, error) { } // NewHeaders is a factory method to create a new basic Http Headers slice. -func NewHeaders() []string { - return []string{ +func NewHeaders(apiKey string) []string { + headers := []string{ "Content-Type: application/json", "Accept: application/json", + fmt.Sprintf("X-NLK-Version: %s", buildinfo.SemVer()), } -} -// NewTlsConfig is a factory method to create a new basic Tls Config. -// More attention should be given to the use of `InsecureSkipVerify: true`, as it is not recommended for production use. -func NewTlsConfig(settings *configuration.Settings) *tls.Config { - tlsConfig, err := authentication.NewTlsConfig(settings) - if err != nil { - logrus.Warnf("Failed to create TLS config: %v", err) - return &tls.Config{InsecureSkipVerify: true} + if apiKey != "" { + headers = append(headers, fmt.Sprintf("Authorization: ApiKey %s", apiKey)) } - return tlsConfig + return headers } // NewTransport is a factory method to create a new basic Http Transport. -func NewTransport(config *tls.Config) *netHttp.Transport { - transport := netHttp.DefaultTransport.(*netHttp.Transport) - transport.TLSClientConfig = config +func NewTransport(skipVerify bool) *netHttp.Transport { + transport := netHttp.DefaultTransport.(*netHttp.Transport).Clone() + transport.TLSClientConfig.InsecureSkipVerify = skipVerify return transport } diff --git a/internal/communication/factory_test.go b/internal/communication/factory_test.go index f25abef..8c637a8 100644 --- a/internal/communication/factory_test.go +++ b/internal/communication/factory_test.go @@ -6,17 +6,15 @@ package communication import ( - "context" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "k8s.io/client-go/kubernetes/fake" "testing" + + "github.com/stretchr/testify/require" ) -func TestNewHttpClient(t *testing.T) { - k8sClient := fake.NewSimpleClientset() - settings, err := configuration.NewSettings(context.Background(), k8sClient) - client, err := NewHttpClient(settings) +func TestNewHTTPClient(t *testing.T) { + t.Parallel() + client, err := NewHTTPClient("fakeKey", true) if err != nil { t.Fatalf(`Unexpected error: %v`, err) } @@ -26,14 +24,45 @@ func TestNewHttpClient(t *testing.T) { } } +//nolint:goconst func TestNewHeaders(t *testing.T) { - headers := NewHeaders() + t.Parallel() + headers := NewHeaders("fakeKey") if headers == nil { t.Fatalf(`headers should not be nil`) } - if len(headers) != 2 { + if len(headers) != 4 { + t.Fatalf(`headers should have 3 elements`) + } + + if headers[0] != "Content-Type: application/json" { + t.Fatalf(`headers[0] should be "Content-Type: application/json"`) + } + + if headers[1] != "Accept: application/json" { + t.Fatalf(`headers[1] should be "Accept: application/json"`) + } + + if headers[2] != "X-NLK-Version: " { + t.Fatalf(`headers[2] should be "X-NLK-Version: "`) + } + + if headers[3] != "Authorization: ApiKey fakeKey" { + t.Fatalf(`headers[3] should be "Accept: Authorization: ApiKey fakeKey"`) + } +} + +func TestNewHeadersWithNoAPIKey(t *testing.T) { + t.Parallel() + headers := NewHeaders("") + + if headers == nil { + t.Fatalf(`headers should not be nil`) + } + + if len(headers) != 3 { t.Fatalf(`headers should have 2 elements`) } @@ -44,13 +73,16 @@ func TestNewHeaders(t *testing.T) { if headers[1] != "Accept: application/json" { t.Fatalf(`headers[1] should be "Accept: application/json"`) } + + if headers[2] != "X-NLK-Version: " { + t.Fatalf(`headers[2] should be "X-NLK-Version: "`) + } } func TestNewTransport(t *testing.T) { - k8sClient := fake.NewSimpleClientset() - settings, _ := configuration.NewSettings(context.Background(), k8sClient) - config := NewTlsConfig(settings) - transport := NewTransport(config) + t.Parallel() + + transport := NewTransport(false) if transport == nil { t.Fatalf(`transport should not be nil`) @@ -60,11 +92,5 @@ func TestNewTransport(t *testing.T) { t.Fatalf(`transport.TLSClientConfig should not be nil`) } - if transport.TLSClientConfig != config { - t.Fatalf(`transport.TLSClientConfig should be the same as config`) - } - - if !transport.TLSClientConfig.InsecureSkipVerify { - t.Fatalf(`transport.TLSClientConfig.InsecureSkipVerify should be true`) - } + require.False(t, transport.TLSClientConfig.InsecureSkipVerify) } diff --git a/internal/communication/roundtripper.go b/internal/communication/roundtripper.go index 3781c62..1dbaf5b 100644 --- a/internal/communication/roundtripper.go +++ b/internal/communication/roundtripper.go @@ -6,11 +6,13 @@ package communication import ( + "errors" "net/http" - netHttp "net/http" "strings" ) +const maxHeaders = 1000 + // RoundTripper is a simple type that wraps the default net/communication RoundTripper to add additional headers. type RoundTripper struct { Headers []string @@ -18,7 +20,7 @@ type RoundTripper struct { } // NewRoundTripper is a factory method to create a new RoundTripper. -func NewRoundTripper(headers []string, transport *netHttp.Transport) *RoundTripper { +func NewRoundTripper(headers []string, transport *http.Transport) *RoundTripper { return &RoundTripper{ Headers: headers, RoundTripper: transport, @@ -27,6 +29,10 @@ func NewRoundTripper(headers []string, transport *netHttp.Transport) *RoundTripp // RoundTrip This simply adds our default headers to the request before passing it on to the default RoundTripper. func (roundTripper *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if len(req.Header) > maxHeaders { + return nil, errors.New("request includes too many headers") + } + newRequest := new(http.Request) *newRequest = *req newRequest.Header = make(http.Header, len(req.Header)) diff --git a/internal/communication/roundtripper_test.go b/internal/communication/roundtripper_test.go index 71620c3..d00af17 100644 --- a/internal/communication/roundtripper_test.go +++ b/internal/communication/roundtripper_test.go @@ -7,21 +7,15 @@ package communication import ( "bytes" - "context" - "fmt" netHttp "net/http" - "net/http/httptest" "testing" - - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "k8s.io/client-go/kubernetes/fake" ) func TestNewRoundTripper(t *testing.T) { - k8sClient := fake.NewSimpleClientset() - settings, _ := configuration.NewSettings(context.Background(), k8sClient) - headers := NewHeaders() - transport := NewTransport(NewTlsConfig(settings)) + t.Parallel() + + headers := NewHeaders("fakeKey") + transport := NewTransport(true) roundTripper := NewRoundTripper(headers, transport) if roundTripper == nil { @@ -32,8 +26,8 @@ func TestNewRoundTripper(t *testing.T) { t.Fatalf(`roundTripper.Headers should not be nil`) } - if len(roundTripper.Headers) != 2 { - t.Fatalf(`roundTripper.Headers should have 2 elements`) + if len(roundTripper.Headers) != 4 { + t.Fatalf(`roundTripper.Headers should have 3 elements`) } if roundTripper.Headers[0] != "Content-Type: application/json" { @@ -44,55 +38,47 @@ func TestNewRoundTripper(t *testing.T) { t.Fatalf(`roundTripper.Headers[1] should be "Accept: application/json"`) } + if roundTripper.Headers[2] != "X-NLK-Version: " { + t.Fatalf(`roundTripper.Headers[2] should be "X-NLK-Version: "`) + } + + if roundTripper.Headers[3] != "Authorization: ApiKey fakeKey" { + t.Fatalf(`roundTripper.Headers[3] should be "Accept: Authorization: ApiKey fakeKey"`) + } + if roundTripper.RoundTripper == nil { t.Fatalf(`roundTripper.RoundTripper should not be nil`) } } func TestRoundTripperRoundTrip(t *testing.T) { - // Create a mock HTTP server - mockServer := httptest.NewServer(netHttp.HandlerFunc(func(w netHttp.ResponseWriter, r *netHttp.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("x-mock-header", "test-value") - w.WriteHeader(netHttp.StatusOK) - fmt.Fprintln(w, `{"message": "mock response"}`) - })) - defer mockServer.Close() - - // Initialize dependencies - k8sClient := fake.NewSimpleClientset() - settings, err := configuration.NewSettings(context.Background(), k8sClient) - if err != nil { - t.Fatalf("Unexpected error creating settings: %v", err) - } + t.Parallel() - headers := NewHeaders() - transport := NewTransport(NewTlsConfig(settings)) + headers := NewHeaders("fakeKey") + transport := NewTransport(true) roundTripper := NewRoundTripper(headers, transport) - // Use the mock server URL - request, err := NewRequest("GET", mockServer.URL, nil) + request, err := NewRequest("GET", "http://example.com", nil) if err != nil { - t.Fatalf("Unexpected error: %v", err) + t.Fatalf(`Unexpected error: %v`, err) } request.Header.Set("Content-Type", "application/json") request.Header.Set("x-nginx-loadbalancer-kubernetes", "nlk") - // Perform the request response, err := roundTripper.RoundTrip(request) if err != nil { - t.Fatalf("Unexpected error: %v", err) + t.Fatalf(`Unexpected error: %v`, err) } if response == nil { - t.Fatalf("Response should not be nil") + t.Fatalf(`response should not be nil`) } + defer response.Body.Close() - // Validate response headers headerLen := len(response.Header) - if headerLen <= 2 { - t.Fatalf("Response headers should have at least 2 elements, found %d", headerLen) + if headerLen <= 3 { + t.Fatalf(`response.Header should have at least 3 elements, found %d`, headerLen) } } diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go new file mode 100644 index 0000000..45764d6 --- /dev/null +++ b/internal/configuration/configuration_test.go @@ -0,0 +1,138 @@ +package configuration_test + +import ( + "testing" + "time" + + "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" + + "github.com/stretchr/testify/require" +) + +func TestConfiguration_Read(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testFile string + expectedSettings configuration.Settings + }{ + "one nginx plus host": { + testFile: "one-nginx-host", + expectedSettings: configuration.Settings{ + LogLevel: "warn", + NginxPlusHosts: []string{"https://10.0.0.1:9000/api"}, + SkipVerifyTLS: false, + Handler: configuration.HandlerSettings{ + RetryCount: 5, + Threads: 1, + WorkQueueSettings: configuration.WorkQueueSettings{ + RateLimiterBase: time.Second * 2, + RateLimiterMax: time.Second * 60, + Name: "nlk-handler", + }, + }, + Synchronizer: configuration.SynchronizerSettings{ + MaxMillisecondsJitter: 750, + MinMillisecondsJitter: 250, + RetryCount: 5, + Threads: 1, + WorkQueueSettings: configuration.WorkQueueSettings{ + RateLimiterBase: time.Second * 2, + RateLimiterMax: time.Second * 60, + Name: "nlk-synchronizer", + }, + }, + Watcher: configuration.WatcherSettings{ + ResyncPeriod: 0, + ServiceAnnotation: "fakeServiceMatch", + }, + }, + }, + "multiple nginx plus hosts": { + testFile: "multiple-nginx-hosts", + expectedSettings: configuration.Settings{ + LogLevel: "warn", + NginxPlusHosts: []string{"https://10.0.0.1:9000/api", "https://10.0.0.2:9000/api"}, + SkipVerifyTLS: true, + Handler: configuration.HandlerSettings{ + RetryCount: 5, + Threads: 1, + WorkQueueSettings: configuration.WorkQueueSettings{ + RateLimiterBase: time.Second * 2, + RateLimiterMax: time.Second * 60, + Name: "nlk-handler", + }, + }, + Synchronizer: configuration.SynchronizerSettings{ + MaxMillisecondsJitter: 750, + MinMillisecondsJitter: 250, + RetryCount: 5, + Threads: 1, + WorkQueueSettings: configuration.WorkQueueSettings{ + RateLimiterBase: time.Second * 2, + RateLimiterMax: time.Second * 60, + Name: "nlk-synchronizer", + }, + }, + Watcher: configuration.WatcherSettings{ + ResyncPeriod: 0, + ServiceAnnotation: "fakeServiceMatch", + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + settings, err := configuration.Read(tc.testFile, "./testdata") + require.NoError(t, err) + require.Equal(t, tc.expectedSettings, settings) + }) + } +} + +func TestConfiguration_TLS(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + tlsMode string + expectedSkipVerifyTLS bool + expectedErr bool + }{ + "no input": { + tlsMode: "", + expectedSkipVerifyTLS: false, + }, + "no tls": { + tlsMode: "no-tls", + expectedSkipVerifyTLS: true, + }, + "skip verify tls": { + tlsMode: "skip-verify-tls", + expectedSkipVerifyTLS: true, + }, + "ca tls": { + tlsMode: "ca-tls", + expectedSkipVerifyTLS: false, + }, + "unexpected input": { + tlsMode: "unexpected-tls-mode", + expectedErr: true, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + skipVerifyTLS, err := configuration.ValidateTLSMode(tc.tlsMode) + if tc.expectedErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectedSkipVerifyTLS, skipVerifyTLS) + }) + } +} diff --git a/internal/configuration/settings.go b/internal/configuration/settings.go index d15ad84..75cec2e 100644 --- a/internal/configuration/settings.go +++ b/internal/configuration/settings.go @@ -6,18 +6,12 @@ package configuration import ( - "context" + "encoding/base64" "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/certification" - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/informers" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" - "strings" + "log/slog" "time" + + "github.com/spf13/viper" ) const ( @@ -39,13 +33,22 @@ const ( // The value of the annotation determines which BorderServer implementation will be used. // See the documentation in the `application/application_constants.go` file for details. PortAnnotationPrefix = "nginxinc.io" + + // ServiceAnnotationMatchKey is the key name of the annotation in the application's config map + // that identifies the ingress service whose events will be monitored. + ServiceAnnotationMatchKey = "service-annotation-match" + + // DefaultServiceAnnotation is the default name of the ingress service whose events will be + // monitored. + DefaultServiceAnnotation = "nginxaas" ) // WorkQueueSettings contains the configuration values needed by the Work Queues. // There are two work queues in the application: // 1. nlk-handler queue, used to move messages between the Watcher and the Handler. // 2. nlk-synchronizer queue, used to move message between the Handler and the Synchronizer. -// The queues are NamedDelayingQueue objects that use an ItemExponentialFailureRateLimiter as the underlying rate limiter. +// The queues are NamedDelayingQueue objects that use an ItemExponentialFailureRateLimiter +// as the underlying rate limiter. type WorkQueueSettings struct { // Name is the name of the queue. Name string @@ -60,7 +63,6 @@ type WorkQueueSettings struct { // HandlerSettings contains the configuration values needed by the Handler. type HandlerSettings struct { - // RetryCount is the number of times the Handler will attempt to process a message before giving up. RetryCount int @@ -73,9 +75,8 @@ type HandlerSettings struct { // WatcherSettings contains the configuration values needed by the Watcher. type WatcherSettings struct { - - // NginxIngressNamespace is the namespace used to filter Services in the Watcher. - NginxIngressNamespace string + // ServiceAnnotation is the annotation of the ingress service whose events the watcher should monitor. + ServiceAnnotation string // ResyncPeriod is the value used to set the resync period for the underlying SharedInformer. ResyncPeriod time.Duration @@ -83,7 +84,6 @@ type WatcherSettings struct { // SynchronizerSettings contains the configuration values needed by the Synchronizer. type SynchronizerSettings struct { - // MaxMillisecondsJitter is the maximum number of milliseconds that will be applied when adding an event to the queue. MaxMillisecondsJitter int @@ -102,27 +102,17 @@ type SynchronizerSettings struct { // Settings contains the configuration values needed by the application. type Settings struct { - - // Context is the context used to control the application. - Context context.Context + // LogLevel is the user-specified log level. Defaults to warn. + LogLevel string // NginxPlusHosts is a list of Nginx Plus hosts that will be used to update the Border Servers. NginxPlusHosts []string - // TlsMode is the value used to determine which of the five TLS modes will be used to communicate with the Border Servers (see: ../../docs/tls/README.md). - TlsMode TLSMode + // SkipVerifyTLS determines whether the http client will skip TLS verification or not. + SkipVerifyTLS bool - // Certificates is the object used to retrieve the certificates and keys used to communicate with the Border Servers. - Certificates *certification.Certificates - - // K8sClient is the Kubernetes client used to communicate with the Kubernetes API. - K8sClient kubernetes.Interface - - // informer is the SharedInformer used to watch for changes to the ConfigMap . - informer cache.SharedInformer - - // eventHandlerRegistration is the object used to track the event handlers with the SharedInformer. - eventHandlerRegistration cache.ResourceEventHandlerRegistration + // APIKey is the api key used to authenticate with the dataplane API. + APIKey string // Handler contains the configuration values needed by the Handler. Handler HandlerSettings @@ -134,13 +124,39 @@ type Settings struct { Watcher WatcherSettings } -// NewSettings creates a new Settings object with default values. -func NewSettings(ctx context.Context, k8sClient kubernetes.Interface) (*Settings, error) { - settings := &Settings{ - Context: ctx, - K8sClient: k8sClient, - TlsMode: NoTLS, - Certificates: nil, +// Read parses all the config and returns the values +func Read(configName, configPath string) (s Settings, err error) { + v := viper.New() + v.SetConfigName(configName) + v.SetConfigType("yaml") + v.AddConfigPath(configPath) + if err = v.ReadInConfig(); err != nil { + return s, err + } + + if err = v.BindEnv("NGINXAAS_DATAPLANE_API_KEY"); err != nil { + return s, err + } + + skipVerifyTLS, err := ValidateTLSMode(v.GetString("tls-mode")) + if err != nil { + slog.Error("could not validate tls mode", "error", err) + } + + if skipVerifyTLS { + slog.Warn("skipping TLS verification for NGINX hosts") + } + + serviceAnnotation := DefaultServiceAnnotation + if sa := v.GetString(ServiceAnnotationMatchKey); sa != "" { + serviceAnnotation = sa + } + + return Settings{ + LogLevel: v.GetString("log-level"), + NginxPlusHosts: v.GetStringSlice("nginx-hosts"), + SkipVerifyTLS: skipVerifyTLS, + APIKey: base64.StdEncoding.EncodeToString([]byte(v.GetString("NGINXAAS_DATAPLANE_API_KEY"))), Handler: HandlerSettings{ RetryCount: 5, Threads: 1, @@ -162,208 +178,21 @@ func NewSettings(ctx context.Context, k8sClient kubernetes.Interface) (*Settings }, }, Watcher: WatcherSettings{ - NginxIngressNamespace: "nginx-ingress", - ResyncPeriod: 0, + ResyncPeriod: 0, + ServiceAnnotation: serviceAnnotation, }, - } - - return settings, nil -} - -// Initialize initializes the Settings object. Sets up a SharedInformer to watch for changes to the ConfigMap. -// This method must be called before the Run method. -func (s *Settings) Initialize() error { - logrus.Info("Settings::Initialize") - - var err error - - certificates := certification.NewCertificates(s.Context, s.K8sClient) - - err = certificates.Initialize() - if err != nil { - return fmt.Errorf(`error occurred initializing certificates: %w`, err) - } - - s.Certificates = certificates - - go certificates.Run() - - logrus.Debug(">>>>>>>>>> Settings::Initialize: retrieving nlk-config ConfigMap") - configMap, err := s.K8sClient.CoreV1().ConfigMaps(ConfigMapsNamespace).Get(s.Context, "nlk-config", metav1.GetOptions{}) - if err != nil { - return err - } - - s.handleUpdateEvent(nil, configMap) - logrus.Debug(">>>>>>>>>> Settings::Initialize: retrieved nlk-config ConfigMap") - - informer, err := s.buildInformer() - if err != nil { - return fmt.Errorf(`error occurred building ConfigMap informer: %w`, err) - } - - s.informer = informer - - err = s.initializeEventListeners() - if err != nil { - return fmt.Errorf(`error occurred initializing event listeners: %w`, err) - } - - return nil -} - -// Run starts the SharedInformer and waits for the Context to be cancelled. -func (s *Settings) Run() { - logrus.Debug("Settings::Run") - - defer utilruntime.HandleCrash() - - go s.informer.Run(s.Context.Done()) - - <-s.Context.Done() -} - -func (s *Settings) buildInformer() (cache.SharedInformer, error) { - options := informers.WithNamespace(ConfigMapsNamespace) - factory := informers.NewSharedInformerFactoryWithOptions(s.K8sClient, ResyncPeriod, options) - informer := factory.Core().V1().ConfigMaps().Informer() - - return informer, nil -} - -func (s *Settings) initializeEventListeners() error { - logrus.Debug("Settings::initializeEventListeners") - - var err error - - handlers := cache.ResourceEventHandlerFuncs{ - AddFunc: s.handleAddEvent, - UpdateFunc: s.handleUpdateEvent, - DeleteFunc: s.handleDeleteEvent, - } - - s.eventHandlerRegistration, err = s.informer.AddEventHandler(handlers) - if err != nil { - return fmt.Errorf(`error occurred registering event handlers: %w`, err) - } - - return nil -} - -func (s *Settings) handleAddEvent(obj interface{}) { - logrus.Debug("Settings::handleAddEvent") - - if _, yes := isOurConfig(obj); yes { - s.handleUpdateEvent(nil, obj) - } -} - -func (s *Settings) handleDeleteEvent(obj interface{}) { - logrus.Debug("Settings::handleDeleteEvent") - - if _, yes := isOurConfig(obj); yes { - s.updateHosts([]string{}) - } -} - -func (s *Settings) handleUpdateEvent(_ interface{}, newValue interface{}) { - logrus.Debug("Settings::handleUpdateEvent") - - configMap, yes := isOurConfig(newValue) - if !yes { - return - } - - hosts, found := configMap.Data["nginx-hosts"] - if found { - newHosts := s.parseHosts(hosts) - s.updateHosts(newHosts) - } else { - logrus.Warnf("Settings::handleUpdateEvent: nginx-hosts key not found in ConfigMap") - } - - tlsMode, err := validateTlsMode(configMap) - if err != nil { - // NOTE: the TLSMode defaults to NoTLS on startup, or the last known good value if previously set. - logrus.Errorf("There was an error with the configured TLS Mode. TLS Mode has NOT been changed. The current mode is: '%v'. Error: %v. ", s.TlsMode, err) - } else { - s.TlsMode = tlsMode - } - - caCertificateSecretKey, found := configMap.Data["ca-certificate"] - if found { - s.Certificates.CaCertificateSecretKey = caCertificateSecretKey - logrus.Debugf("Settings::handleUpdateEvent: ca-certificate: %s", s.Certificates.CaCertificateSecretKey) - } else { - s.Certificates.CaCertificateSecretKey = "" - logrus.Warnf("Settings::handleUpdateEvent: ca-certificate key not found in ConfigMap") - } - - clientCertificateSecretKey, found := configMap.Data["client-certificate"] - if found { - s.Certificates.ClientCertificateSecretKey = clientCertificateSecretKey - logrus.Debugf("Settings::handleUpdateEvent: client-certificate: %s", s.Certificates.ClientCertificateSecretKey) - } else { - s.Certificates.ClientCertificateSecretKey = "" - logrus.Warnf("Settings::handleUpdateEvent: client-certificate key not found in ConfigMap") - } - - setLogLevel(configMap.Data["log-level"]) - - logrus.Debugf("Settings::handleUpdateEvent: \n\tHosts: %v,\n\tSettings: %v ", s.NginxPlusHosts, configMap) + }, nil } -func validateTlsMode(configMap *corev1.ConfigMap) (TLSMode, error) { - tlsConfigMode, tlsConfigModeFound := configMap.Data["tls-mode"] - if !tlsConfigModeFound { - return NoTLS, fmt.Errorf(`tls-mode key not found in ConfigMap`) +func ValidateTLSMode(tlsConfigMode string) (skipVerify bool, err error) { + if tlsConfigMode == "" { + return false, nil } - if tlsMode, tlsModeFound := TLSModeMap[tlsConfigMode]; tlsModeFound { - return tlsMode, nil + var tlsModeFound bool + if skipVerify, tlsModeFound = tlsModeMap[tlsConfigMode]; tlsModeFound { + return skipVerify, nil } - return NoTLS, fmt.Errorf(`invalid tls-mode value: %s`, tlsConfigMode) -} - -func (s *Settings) parseHosts(hosts string) []string { - return strings.Split(hosts, ",") -} - -func (s *Settings) updateHosts(hosts []string) { - s.NginxPlusHosts = hosts -} - -func isOurConfig(obj interface{}) (*corev1.ConfigMap, bool) { - configMap, ok := obj.(*corev1.ConfigMap) - return configMap, ok && configMap.Name == ConfigMapName && configMap.Namespace == ConfigMapsNamespace -} - -func setLogLevel(logLevel string) { - logrus.Debugf("Settings::setLogLevel: %s", logLevel) - switch logLevel { - case "panic": - logrus.SetLevel(logrus.PanicLevel) - - case "fatal": - logrus.SetLevel(logrus.FatalLevel) - - case "error": - logrus.SetLevel(logrus.ErrorLevel) - - case "warn": - logrus.SetLevel(logrus.WarnLevel) - - case "info": - logrus.SetLevel(logrus.InfoLevel) - - case "debug": - logrus.SetLevel(logrus.DebugLevel) - - case "trace": - logrus.SetLevel(logrus.TraceLevel) - - default: - logrus.SetLevel(logrus.WarnLevel) - } + return false, fmt.Errorf(`invalid tls-mode value: %s`, tlsConfigMode) } diff --git a/internal/configuration/testdata/multiple-nginx-hosts.yaml b/internal/configuration/testdata/multiple-nginx-hosts.yaml new file mode 100644 index 0000000..2235c1a --- /dev/null +++ b/internal/configuration/testdata/multiple-nginx-hosts.yaml @@ -0,0 +1,11 @@ +ca-certificate: "fakeCAKey" +client-certificate: "fakeCertKey" +log-level: "warn" +nginx-hosts: ["https://10.0.0.1:9000/api", "https://10.0.0.2:9000/api"] +tls-mode: "no-tls" +service-annotation-match: "fakeServiceMatch" +creationTimestamp: "2024-09-04T17:59:20Z" +name: "nlk-config" +namespace: "nlk" +resourceVersion: "5909" +uid: "66d49974-49d6-4ad8-8135-dcebda7b5c9e" diff --git a/internal/configuration/testdata/one-nginx-host.yaml b/internal/configuration/testdata/one-nginx-host.yaml new file mode 100644 index 0000000..45e55ef --- /dev/null +++ b/internal/configuration/testdata/one-nginx-host.yaml @@ -0,0 +1,10 @@ +ca-certificate: "fakeCAKey" +client-certificate: "fakeCertKey" +log-level: "warn" +nginx-hosts: "https://10.0.0.1:9000/api" +service-annotation-match: "fakeServiceMatch" +creationTimestamp: "2024-09-04T17:59:20Z" +name: "nlk-config" +namespace: "nlk" +resourceVersion: "5909" +uid: "66d49974-49d6-4ad8-8135-dcebda7b5c9e" diff --git a/internal/configuration/tlsmodes.go b/internal/configuration/tlsmodes.go index 2f7271f..e329047 100644 --- a/internal/configuration/tlsmodes.go +++ b/internal/configuration/tlsmodes.go @@ -6,41 +6,19 @@ package configuration const ( - NoTLS TLSMode = iota - CertificateAuthorityTLS - CertificateAuthorityMutualTLS - SelfSignedTLS - SelfSignedMutualTLS + // NoTLS is deprecated as misleading. It is the same as SkipVerifyTLS. + NoTLS = "no-tls" + // SkipVerifyTLS causes the http client to skip verification of the NGINX + // host's certificate chain and host name. + SkipVerifyTLS = "skip-verify-tls" + // CertificateAuthorityTLS is deprecated as misleading. This is the same as + // the default behavior which is to verify the NGINX hosts's certificate + // chain and host name, if https is used. + CertificateAuthorityTLS = "ca-tls" ) -const ( - NoTLSString = "no-tls" - CertificateAuthorityTLSString = "ca-tls" - CertificateAuthorityMutualTLSString = "ca-mtls" - SelfSignedTLSString = "ss-tls" - SelfSignedMutualTLSString = "ss-mtls" -) - -type TLSMode int - -var TLSModeMap = map[string]TLSMode{ - NoTLSString: NoTLS, - CertificateAuthorityTLSString: CertificateAuthorityTLS, - CertificateAuthorityMutualTLSString: CertificateAuthorityMutualTLS, - SelfSignedTLSString: SelfSignedTLS, - SelfSignedMutualTLSString: SelfSignedMutualTLS, -} - -func (t TLSMode) String() string { - modes := []string{ - NoTLSString, - CertificateAuthorityTLSString, - CertificateAuthorityMutualTLSString, - SelfSignedTLSString, - SelfSignedMutualTLSString, - } - if t < NoTLS || t > SelfSignedMutualTLS { - return "" - } - return modes[t] +var tlsModeMap = map[string]bool{ + NoTLS: true, + SkipVerifyTLS: true, + CertificateAuthorityTLS: false, } diff --git a/internal/configuration/tlsmodes_test.go b/internal/configuration/tlsmodes_test.go deleted file mode 100644 index 62abf96..0000000 --- a/internal/configuration/tlsmodes_test.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package configuration - -import ( - "testing" -) - -func Test_String(t *testing.T) { - mode := NoTLS.String() - if mode != "no-tls" { - t.Errorf("Expected TLSModeNoTLS to be 'no-tls', got '%s'", mode) - } - - mode = CertificateAuthorityTLS.String() - if mode != "ca-tls" { - t.Errorf("Expected TLSModeCaTLS to be 'ca-tls', got '%s'", mode) - } - - mode = CertificateAuthorityMutualTLS.String() - if mode != "ca-mtls" { - t.Errorf("Expected TLSModeCaMTLS to be 'ca-mtls', got '%s'", mode) - } - - mode = SelfSignedTLS.String() - if mode != "ss-tls" { - t.Errorf("Expected TLSModeSsTLS to be 'ss-tls', got '%s'", mode) - } - - mode = SelfSignedMutualTLS.String() - if mode != "ss-mtls" { - t.Errorf("Expected TLSModeSsMTLS to be 'ss-mtls', got '%s',", mode) - } - - mode = TLSMode(5).String() - if mode != "" { - t.Errorf("Expected TLSMode(5) to be '', got '%s'", mode) - } -} - -func Test_TLSModeMap(t *testing.T) { - mode := TLSModeMap["no-tls"] - if mode != NoTLS { - t.Errorf("Expected TLSModeMap['no-tls'] to be TLSModeNoTLS, got '%d'", mode) - } - - mode = TLSModeMap["ca-tls"] - if mode != CertificateAuthorityTLS { - t.Errorf("Expected TLSModeMap['ca-tls'] to be TLSModeCaTLS, got '%d'", mode) - } - - mode = TLSModeMap["ca-mtls"] - if mode != CertificateAuthorityMutualTLS { - t.Errorf("Expected TLSModeMap['ca-mtls'] to be TLSModeCaMTLS, got '%d'", mode) - } - - mode = TLSModeMap["ss-tls"] - if mode != SelfSignedTLS { - t.Errorf("Expected TLSModeMap['ss-tls'] to be TLSModeSsTLS, got '%d'", mode) - } - - mode = TLSModeMap["ss-mtls"] - if mode != SelfSignedMutualTLS { - t.Errorf("Expected TLSModeMap['ss-mtls'] to be TLSModeSsMTLS, got '%d'", mode) - } - - mode = TLSModeMap["invalid"] - if mode != TLSMode(0) { - t.Errorf("Expected TLSModeMap['invalid'] to be TLSMode(0), got '%d'", mode) - } -} diff --git a/internal/core/event.go b/internal/core/event.go index 09776c9..c32511e 100644 --- a/internal/core/event.go +++ b/internal/core/event.go @@ -24,27 +24,17 @@ const ( // Event represents a service event type Event struct { - // Type represents the event type, one of the constant values defined above. Type EventType // Service represents the service object in its current state Service *v1.Service - - // PreviousService represents the service object in its previous state - PreviousService *v1.Service - - // NodeIps represents the list of node IPs in the Cluster. This is populated by the Watcher when an event is created. - // The Node IPs are needed by the BorderClient. - NodeIps []string } // NewEvent factory method to create a new Event -func NewEvent(eventType EventType, service *v1.Service, previousService *v1.Service, nodeIps []string) Event { +func NewEvent(eventType EventType, service *v1.Service) Event { return Event{ - Type: eventType, - Service: service, - PreviousService: previousService, - NodeIps: nodeIps, + Type: eventType, + Service: service, } } diff --git a/internal/core/event_test.go b/internal/core/event_test.go index b3b8926..09724cf 100644 --- a/internal/core/event_test.go +++ b/internal/core/event_test.go @@ -1,17 +1,17 @@ package core import ( - v1 "k8s.io/api/core/v1" "testing" + + v1 "k8s.io/api/core/v1" ) func TestNewEvent(t *testing.T) { + t.Parallel() expectedType := Created expectedService := &v1.Service{} - expectedPreviousService := &v1.Service{} - expectedNodeIps := []string{"127.0.0.1"} - event := NewEvent(expectedType, expectedService, expectedPreviousService, expectedNodeIps) + event := NewEvent(expectedType, expectedService) if event.Type != expectedType { t.Errorf("expected Created, got %v", event.Type) @@ -20,12 +20,4 @@ func TestNewEvent(t *testing.T) { if event.Service != expectedService { t.Errorf("expected service, got %#v", event.Service) } - - if event.PreviousService != expectedPreviousService { - t.Errorf("expected previous service, got %#v", event.PreviousService) - } - - if event.NodeIps[0] != expectedNodeIps[0] { - t.Errorf("expected node ips, got %#v", event.NodeIps) - } } diff --git a/internal/core/secret_bytes.go b/internal/core/secret_bytes.go deleted file mode 100644 index 0bbc3bf..0000000 --- a/internal/core/secret_bytes.go +++ /dev/null @@ -1,21 +0,0 @@ -package core - -import ( - "encoding/json" -) - -// Wraps byte slices which potentially could contain -// sensitive data that should not be output to the logs. -// This will output [REDACTED] if attempts are made -// to print this type in logs, serialize to JSON, or -// otherwise convert it to a string. -// Usage: core.SecretBytes(myByteSlice) -type SecretBytes []byte - -func (sb SecretBytes) String() string { - return "[REDACTED]" -} - -func (sb SecretBytes) MarshalJSON() ([]byte, error) { - return json.Marshal("[REDACTED]") -} diff --git a/internal/core/secret_bytes_test.go b/internal/core/secret_bytes_test.go deleted file mode 100644 index 8dd8024..0000000 --- a/internal/core/secret_bytes_test.go +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package core - -import ( - "encoding/json" - "fmt" - "testing" -) - -func TestSecretBytesToString(t *testing.T) { - sensitive := SecretBytes([]byte("If you can see this we have a problem")) - - expected := "foo [REDACTED] bar" - result := fmt.Sprintf("foo %v bar", sensitive) - if result != expected { - t.Errorf("Expected %s, got %s", expected, result) - } -} - -func TestSecretBytesToJSON(t *testing.T) { - sensitive, _ := json.Marshal(SecretBytes([]byte("If you can see this we have a problem"))) - expected := `foo "[REDACTED]" bar` - result := fmt.Sprintf("foo %v bar", string(sensitive)) - if result != expected { - t.Errorf("Expected %s, got %s", expected, result) - } -} diff --git a/internal/core/server_update_event.go b/internal/core/server_update_event.go index f3961ea..04d2dc0 100644 --- a/internal/core/server_update_event.go +++ b/internal/core/server_update_event.go @@ -9,14 +9,10 @@ package core // from Events received from the Handler. These are then consumed by the Synchronizer and passed along to // the appropriate BorderClient. type ServerUpdateEvent struct { - // ClientType is the type of BorderClient that should handle this event. This is configured via Service Annotations. // See application_constants.go for the list of supported types. ClientType string - // Id is the unique identifier for this event. - Id string - // NginxHost is the host name of the NGINX Plus instance that should handle this event. NginxHost string @@ -34,7 +30,12 @@ type ServerUpdateEvent struct { type ServerUpdateEvents = []*ServerUpdateEvent // NewServerUpdateEvent creates a new ServerUpdateEvent. -func NewServerUpdateEvent(eventType EventType, upstreamName string, clientType string, upstreamServers UpstreamServers) *ServerUpdateEvent { +func NewServerUpdateEvent( + eventType EventType, + upstreamName string, + clientType string, + upstreamServers UpstreamServers, +) *ServerUpdateEvent { return &ServerUpdateEvent{ ClientType: clientType, Type: eventType, @@ -43,11 +44,10 @@ func NewServerUpdateEvent(eventType EventType, upstreamName string, clientType s } } -// ServerUpdateEventWithIdAndHost creates a new ServerUpdateEvent with the specified Id and Host. -func ServerUpdateEventWithIdAndHost(event *ServerUpdateEvent, id string, nginxHost string) *ServerUpdateEvent { +// ServerUpdateEventWithHost creates a new ServerUpdateEvent with the specified Host. +func ServerUpdateEventWithHost(event *ServerUpdateEvent, nginxHost string) *ServerUpdateEvent { return &ServerUpdateEvent{ ClientType: event.ClientType, - Id: id, NginxHost: nginxHost, Type: event.Type, UpstreamName: event.UpstreamName, diff --git a/internal/core/server_update_event_test.go b/internal/core/server_update_event_test.go index a891e23..3be4702 100644 --- a/internal/core/server_update_event_test.go +++ b/internal/core/server_update_event_test.go @@ -14,32 +14,26 @@ const clientType = "clientType" var emptyUpstreamServers UpstreamServers func TestServerUpdateEventWithIdAndHost(t *testing.T) { + t.Parallel() event := NewServerUpdateEvent(Created, "upstream", clientType, emptyUpstreamServers) - if event.Id != "" { - t.Errorf("expected empty Id, got %s", event.Id) - } - if event.NginxHost != "" { t.Errorf("expected empty NginxHost, got %s", event.NginxHost) } - eventWithIdAndHost := ServerUpdateEventWithIdAndHost(event, "id", "host") - - if eventWithIdAndHost.Id != "id" { - t.Errorf("expected Id to be 'id', got %s", eventWithIdAndHost.Id) - } + eventWithIDAndHost := ServerUpdateEventWithHost(event, "host") - if eventWithIdAndHost.NginxHost != "host" { - t.Errorf("expected NginxHost to be 'host', got %s", eventWithIdAndHost.NginxHost) + if eventWithIDAndHost.NginxHost != "host" { + t.Errorf("expected NginxHost to be 'host', got %s", eventWithIDAndHost.NginxHost) } - if eventWithIdAndHost.ClientType != clientType { - t.Errorf("expected ClientType to be '%s', got %s", clientType, eventWithIdAndHost.ClientType) + if eventWithIDAndHost.ClientType != clientType { + t.Errorf("expected ClientType to be '%s', got %s", clientType, eventWithIDAndHost.ClientType) } } func TestTypeNameCreated(t *testing.T) { + t.Parallel() event := NewServerUpdateEvent(Created, "upstream", clientType, emptyUpstreamServers) if event.TypeName() != "Created" { @@ -48,6 +42,7 @@ func TestTypeNameCreated(t *testing.T) { } func TestTypeNameUpdated(t *testing.T) { + t.Parallel() event := NewServerUpdateEvent(Updated, "upstream", clientType, emptyUpstreamServers) if event.TypeName() != "Updated" { @@ -56,6 +51,7 @@ func TestTypeNameUpdated(t *testing.T) { } func TestTypeNameDeleted(t *testing.T) { + t.Parallel() event := NewServerUpdateEvent(Deleted, "upstream", clientType, emptyUpstreamServers) if event.TypeName() != "Deleted" { @@ -64,6 +60,7 @@ func TestTypeNameDeleted(t *testing.T) { } func TestTypeNameUnknown(t *testing.T) { + t.Parallel() event := NewServerUpdateEvent(EventType(100), "upstream", clientType, emptyUpstreamServers) if event.TypeName() != "Unknown" { diff --git a/internal/core/upstream_server.go b/internal/core/upstream_server.go index 7c89b1e..eeb72ac 100644 --- a/internal/core/upstream_server.go +++ b/internal/core/upstream_server.go @@ -5,7 +5,8 @@ package core -// UpstreamServer represents a single upstream server. This is an internal representation used to abstract the definition +// UpstreamServer represents a single upstream server. +// This is an internal representation used to abstract the definition // of an upstream server from any specific client. type UpstreamServer struct { diff --git a/internal/core/upstream_server_test.go b/internal/core/upstream_server_test.go index 7b0eed5..91592cd 100644 --- a/internal/core/upstream_server_test.go +++ b/internal/core/upstream_server_test.go @@ -8,6 +8,7 @@ package core import "testing" func TestNewUpstreamServer(t *testing.T) { + t.Parallel() host := "localhost" us := NewUpstreamServer(host) if us.Host != host { diff --git a/internal/observation/handler.go b/internal/observation/handler.go deleted file mode 100644 index 83601b0..0000000 --- a/internal/observation/handler.go +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package observation - -import ( - "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/synchronization" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/translation" - "github.com/sirupsen/logrus" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/util/workqueue" -) - -// HandlerInterface is the interface for the event handler -type HandlerInterface interface { - - // AddRateLimitedEvent defines the interface for adding an event to the event queue - AddRateLimitedEvent(event *core.Event) - - // Run defines the interface used to start the event handler - Run(stopCh <-chan struct{}) - - // ShutDown defines the interface used to stop the event handler - ShutDown() -} - -// Handler is responsible for processing events in the "nlk-handler" queue. -// When processing a message the Translation module is used to translate the event into an internal representation. -// The translation process may result in multiple events being generated. This fan-out mainly supports the differences -// in NGINX Plus API calls for creating/updating Upstreams and deleting Upstreams. -type Handler struct { - - // eventQueue is the queue used to store events - eventQueue workqueue.RateLimitingInterface - - // settings is the configuration settings - settings *configuration.Settings - - // synchronizer is the synchronizer used to synchronize the internal representation with a Border Server - synchronizer synchronization.Interface -} - -// NewHandler creates a new event handler -func NewHandler(settings *configuration.Settings, synchronizer synchronization.Interface, eventQueue workqueue.RateLimitingInterface) *Handler { - return &Handler{ - eventQueue: eventQueue, - settings: settings, - synchronizer: synchronizer, - } -} - -// AddRateLimitedEvent adds an event to the event queue -func (h *Handler) AddRateLimitedEvent(event *core.Event) { - logrus.Debugf(`Handler::AddRateLimitedEvent: %#v`, event) - h.eventQueue.AddRateLimited(event) -} - -// Run starts the event handler, spins up Goroutines to process events, and waits for a stop signal -func (h *Handler) Run(stopCh <-chan struct{}) { - logrus.Debug("Handler::Run") - - for i := 0; i < h.settings.Handler.Threads; i++ { - go wait.Until(h.worker, 0, stopCh) - } - - <-stopCh -} - -// ShutDown stops the event handler and shuts down the event queue -func (h *Handler) ShutDown() { - logrus.Debug("Handler::ShutDown") - h.eventQueue.ShutDown() -} - -// handleEvent feeds translated events to the synchronizer -func (h *Handler) handleEvent(e *core.Event) error { - logrus.Debugf(`Handler::handleEvent: %#v`, e) - // TODO: Add Telemetry - - events, err := translation.Translate(e) - if err != nil { - return fmt.Errorf(`Handler::handleEvent error translating: %v`, err) - } - - h.synchronizer.AddEvents(events) - - return nil -} - -// handleNextEvent pulls an event from the event queue and feeds it to the event handler with retry logic -func (h *Handler) handleNextEvent() bool { - logrus.Debug("Handler::handleNextEvent") - evt, quit := h.eventQueue.Get() - logrus.Debugf(`Handler::handleNextEvent: %#v, quit: %v`, evt, quit) - if quit { - return false - } - - defer h.eventQueue.Done(evt) - - event := evt.(*core.Event) - h.withRetry(h.handleEvent(event), event) - - return true -} - -// worker is the main message loop -func (h *Handler) worker() { - for h.handleNextEvent() { - // TODO: Add Telemetry - } -} - -// withRetry handles errors from the event handler and requeues events that fail -func (h *Handler) withRetry(err error, event *core.Event) { - logrus.Debug("Handler::withRetry") - if err != nil { - // TODO: Add Telemetry - if h.eventQueue.NumRequeues(event) < h.settings.Handler.RetryCount { - h.eventQueue.AddRateLimited(event) - logrus.Infof(`Handler::withRetry: requeued event: %#v; error: %v`, event, err) - } else { - h.eventQueue.Forget(event) - logrus.Warnf(`Handler::withRetry: event %#v has been dropped due to too many retries`, event) - } - } // TODO: Add error logging -} diff --git a/internal/observation/handler_test.go b/internal/observation/handler_test.go deleted file mode 100644 index f4a617f..0000000 --- a/internal/observation/handler_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package observation - -import ( - "context" - "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" - v1 "k8s.io/api/core/v1" - "k8s.io/client-go/util/workqueue" - "testing" -) - -func TestHandler_AddsEventToSynchronizer(t *testing.T) { - _, _, synchronizer, handler, err := buildHandler() - if err != nil { - t.Errorf(`should have been no error, %v`, err) - } - - event := &core.Event{ - Type: core.Created, - Service: &v1.Service{ - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Name: "nlk-back", - }, - }, - }, - }, - } - - handler.AddRateLimitedEvent(event) - - handler.handleNextEvent() - - if len(synchronizer.Events) != 1 { - t.Errorf(`handler.AddRateLimitedEvent did not add the event to the queue`) - } -} - -func buildHandler() (*configuration.Settings, workqueue.RateLimitingInterface, *mocks.MockSynchronizer, *Handler, error) { - settings, err := configuration.NewSettings(context.Background(), nil) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf(`should have been no error, %v`, err) - } - - eventQueue := &mocks.MockRateLimiter{} - synchronizer := &mocks.MockSynchronizer{} - - handler := NewHandler(settings, synchronizer, eventQueue) - - return settings, eventQueue, synchronizer, handler, nil -} diff --git a/internal/observation/register.go b/internal/observation/register.go new file mode 100644 index 0000000..bfe61f8 --- /dev/null +++ b/internal/observation/register.go @@ -0,0 +1,63 @@ +package observation + +import ( + "sync" + + v1 "k8s.io/api/core/v1" +) + +// register holds references to the services that the user has configured for use with NLK +type register struct { + mu sync.RWMutex // protects register + services map[registerKey]*v1.Service +} + +type registerKey struct { + serviceName string + namespace string +} + +func newRegister() *register { + return ®ister{ + services: make(map[registerKey]*v1.Service), + } +} + +// addOrUpdateService adds the service to the register if not found, else updates the existing service +func (r *register) addOrUpdateService(service *v1.Service) { + r.mu.Lock() + defer r.mu.Unlock() + + r.services[registerKey{namespace: service.Namespace, serviceName: service.Name}] = service +} + +// removeService removes the service from the register +func (r *register) removeService(service *v1.Service) { + r.mu.Lock() + defer r.mu.Unlock() + + delete(r.services, registerKey{namespace: service.Namespace, serviceName: service.Name}) +} + +// getService returns the service from the register if found +func (r *register) getService(namespace string, serviceName string) (*v1.Service, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + s, ok := r.services[registerKey{namespace: namespace, serviceName: serviceName}] + return s, ok +} + +// listServices returns all the services in the register +func (r *register) listServices() []*v1.Service { + r.mu.RLock() + defer r.mu.RUnlock() + + services := make([]*v1.Service, 0, len(r.services)) + + for _, service := range r.services { + services = append(services, service) + } + + return services +} diff --git a/internal/observation/watcher.go b/internal/observation/watcher.go index bf866ee..9711347 100644 --- a/internal/observation/watcher.go +++ b/internal/observation/watcher.go @@ -6,17 +6,18 @@ package observation import ( - "errors" + "context" "fmt" - "time" + "log/slog" "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - "github.com/sirupsen/logrus" + "github.com/nginxinc/kubernetes-nginx-ingress/internal/synchronization" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + discovery "k8s.io/api/discovery/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/informers" + coreinformers "k8s.io/client-go/informers/core/v1" + discoveryinformers "k8s.io/client-go/informers/discovery/v1" "k8s.io/client-go/tools/cache" ) @@ -24,182 +25,285 @@ import ( // Particularly, Services in the namespace defined in the WatcherSettings::NginxIngressNamespace setting. // When a change is detected, an Event is generated and added to the Handler's queue. type Watcher struct { + synchronizer synchronization.Interface - // eventHandlerRegistration is used to track the event handlers - eventHandlerRegistration interface{} + // settings is the configuration settings + settings configuration.Settings - // handler is the event handler - handler HandlerInterface + // servicesInformer is the informer used to watch for changes to services + servicesInformer cache.SharedIndexInformer - // informer is the informer used to watch for changes to Kubernetes resources - informer cache.SharedIndexInformer + // endpointSliceInformer is the informer used to watch for changes to endpoint slices + endpointSliceInformer cache.SharedIndexInformer - // settings is the configuration settings - settings *configuration.Settings + // nodesInformer is the informer used to watch for changes to nodes + nodesInformer cache.SharedIndexInformer + + register *register } // NewWatcher creates a new Watcher -func NewWatcher(settings *configuration.Settings, handler HandlerInterface) (*Watcher, error) { - return &Watcher{ - handler: handler, - settings: settings, - }, nil -} +func NewWatcher( + settings configuration.Settings, + synchronizer synchronization.Interface, + serviceInformer coreinformers.ServiceInformer, + endpointSliceInformer discoveryinformers.EndpointSliceInformer, + nodeInformer coreinformers.NodeInformer, +) (*Watcher, error) { + if serviceInformer == nil { + return nil, fmt.Errorf("service informer cannot be nil") + } -// Initialize initializes the Watcher, must be called before Watch -func (w *Watcher) Initialize() error { - logrus.Debug("Watcher::Initialize") - var err error + if endpointSliceInformer == nil { + return nil, fmt.Errorf("endpoint slice informer cannot be nil") + } - w.informer, err = w.buildInformer() - if err != nil { - return fmt.Errorf(`initialization error: %w`, err) + if nodeInformer == nil { + return nil, fmt.Errorf("node informer cannot be nil") } - err = w.initializeEventListeners() - if err != nil { - return fmt.Errorf(`initialization error: %w`, err) + servicesInformer := serviceInformer.Informer() + endpointSlicesInformer := endpointSliceInformer.Informer() + nodesInformer := nodeInformer.Informer() + + w := &Watcher{ + synchronizer: synchronizer, + settings: settings, + servicesInformer: servicesInformer, + endpointSliceInformer: endpointSlicesInformer, + nodesInformer: nodesInformer, + register: newRegister(), } - return nil + if err := w.initializeEventListeners(servicesInformer); err != nil { + return nil, err + } + + return w, nil } -// Watch starts the process of watching for changes to Kubernetes resources. +// Run starts the process of watching for changes to Kubernetes resources. // Initialize must be called before Watch. -func (w *Watcher) Watch() error { - logrus.Debug("Watcher::Watch") - - if w.informer == nil { - return errors.New("error: Initialize must be called before Watch") +func (w *Watcher) Run(ctx context.Context) error { + if w.servicesInformer == nil { + return fmt.Errorf(`servicesInformer is nil`) } + slog.Debug("Watcher::Watch") + defer utilruntime.HandleCrash() - defer w.handler.ShutDown() + defer w.synchronizer.ShutDown() - go w.informer.Run(w.settings.Context.Done()) + <-ctx.Done() + return nil +} - if !cache.WaitForNamedCacheSync(w.settings.Handler.WorkQueueSettings.Name, w.settings.Context.Done(), w.informer.HasSynced) { - return fmt.Errorf(`error occurred waiting for the cache to sync`) +// isDesiredService returns whether the user has configured the given service for watching. +func (w *Watcher) isDesiredService(service *v1.Service) bool { + annotation, ok := service.Annotations["nginx.com/nginxaas"] + if !ok { + return false } - <-w.settings.Context.Done() - return nil + return annotation == w.settings.Watcher.ServiceAnnotation } -// buildEventHandlerForAdd creates a function that is used as an event handler for the informer when Add events are raised. -func (w *Watcher) buildEventHandlerForAdd() func(interface{}) { - logrus.Info("Watcher::buildEventHandlerForAdd") +func (w *Watcher) buildNodesEventHandlerForAdd() func(interface{}) { + slog.Info("Watcher::buildNodesEventHandlerForAdd") return func(obj interface{}) { - nodeIps, err := w.retrieveNodeIps() - if err != nil { - logrus.Errorf(`error occurred retrieving node ips: %v`, err) - return + slog.Debug("received node add event") + for _, service := range w.register.listServices() { + e := core.NewEvent(core.Updated, service) + w.synchronizer.AddEvent(e) } - service := obj.(*v1.Service) - var previousService *v1.Service - e := core.NewEvent(core.Created, service, previousService, nodeIps) - w.handler.AddRateLimitedEvent(&e) } } -// buildEventHandlerForDelete creates a function that is used as an event handler for the informer when Delete events are raised. -func (w *Watcher) buildEventHandlerForDelete() func(interface{}) { - logrus.Info("Watcher::buildEventHandlerForDelete") +func (w *Watcher) buildNodesEventHandlerForUpdate() func(interface{}, interface{}) { + slog.Info("Watcher::buildNodesEventHandlerForUpdate") + return func(previous, updated interface{}) { + slog.Debug("received node update event") + for _, service := range w.register.listServices() { + e := core.NewEvent(core.Updated, service) + w.synchronizer.AddEvent(e) + } + } +} + +func (w *Watcher) buildNodesEventHandlerForDelete() func(interface{}) { + slog.Info("Watcher::buildNodesEventHandlerForDelete") return func(obj interface{}) { - nodeIps, err := w.retrieveNodeIps() - if err != nil { - logrus.Errorf(`error occurred retrieving node ips: %v`, err) + slog.Debug("received node delete event") + for _, service := range w.register.listServices() { + e := core.NewEvent(core.Updated, service) + w.synchronizer.AddEvent(e) + } + } +} + +func (w *Watcher) buildEndpointSlicesEventHandlerForAdd() func(interface{}) { + slog.Info("Watcher::buildEndpointSlicesEventHandlerForAdd") + return func(obj interface{}) { + slog.Debug("received endpoint slice add event") + endpointSlice, ok := obj.(*discovery.EndpointSlice) + if !ok { + slog.Error("could not convert event object to EndpointSlice", "obj", obj) return } - service := obj.(*v1.Service) - var previousService *v1.Service - e := core.NewEvent(core.Deleted, service, previousService, nodeIps) - w.handler.AddRateLimitedEvent(&e) + + service, ok := w.register.getService(endpointSlice.Namespace, endpointSlice.Labels["kubernetes.io/service-name"]) + if !ok { + // not interested in any unregistered service + return + } + + e := core.NewEvent(core.Updated, service) + w.synchronizer.AddEvent(e) } } -// buildEventHandlerForUpdate creates a function that is used as an event handler for the informer when Update events are raised. -func (w *Watcher) buildEventHandlerForUpdate() func(interface{}, interface{}) { - logrus.Info("Watcher::buildEventHandlerForUpdate") +func (w *Watcher) buildEndpointSlicesEventHandlerForUpdate() func(interface{}, interface{}) { + slog.Info("Watcher::buildEndpointSlicesEventHandlerForUpdate") return func(previous, updated interface{}) { - nodeIps, err := w.retrieveNodeIps() - if err != nil { - logrus.Errorf(`error occurred retrieving node ips: %v`, err) + slog.Debug("received endpoint slice update event") + endpointSlice, ok := updated.(*discovery.EndpointSlice) + if !ok { + slog.Error("could not convert event object to EndpointSlice", "obj", updated) return } - service := updated.(*v1.Service) - previousService := previous.(*v1.Service) - e := core.NewEvent(core.Updated, service, previousService, nodeIps) - w.handler.AddRateLimitedEvent(&e) + + service, ok := w.register.getService(endpointSlice.Namespace, endpointSlice.Labels["kubernetes.io/service-name"]) + if !ok { + // not interested in any unregistered service + return + } + + e := core.NewEvent(core.Updated, service) + w.synchronizer.AddEvent(e) } } -// buildInformer creates the informer used to watch for changes to Kubernetes resources. -func (w *Watcher) buildInformer() (cache.SharedIndexInformer, error) { - logrus.Debug("Watcher::buildInformer") +func (w *Watcher) buildEndpointSlicesEventHandlerForDelete() func(interface{}) { + slog.Info("Watcher::buildEndpointSlicesEventHandlerForDelete") + return func(obj interface{}) { + slog.Debug("received endpoint slice delete event") + endpointSlice, ok := obj.(*discovery.EndpointSlice) + if !ok { + slog.Error("could not convert event object to EndpointSlice", "obj", obj) + return + } - options := informers.WithNamespace(w.settings.Watcher.NginxIngressNamespace) - factory := informers.NewSharedInformerFactoryWithOptions(w.settings.K8sClient, w.settings.Watcher.ResyncPeriod, options) - informer := factory.Core().V1().Services().Informer() + service, ok := w.register.getService(endpointSlice.Namespace, endpointSlice.Labels["kubernetes.io/service-name"]) + if !ok { + // not interested in any unregistered service + return + } - return informer, nil + e := core.NewEvent(core.Deleted, service) + w.synchronizer.AddEvent(e) + } } -// initializeEventListeners initializes the event listeners for the informer. -func (w *Watcher) initializeEventListeners() error { - logrus.Debug("Watcher::initializeEventListeners") - var err error +// buildServiceEventHandlerForAdd creates a function that is used as an event handler +// for the informer when Add events are raised. +func (w *Watcher) buildServiceEventHandlerForAdd() func(interface{}) { + slog.Info("Watcher::buildServiceEventHandlerForAdd") + return func(obj interface{}) { + service := obj.(*v1.Service) + if !w.isDesiredService(service) { + return + } - handlers := cache.ResourceEventHandlerFuncs{ - AddFunc: w.buildEventHandlerForAdd(), - DeleteFunc: w.buildEventHandlerForDelete(), - UpdateFunc: w.buildEventHandlerForUpdate(), - } + w.register.addOrUpdateService(service) - w.eventHandlerRegistration, err = w.informer.AddEventHandler(handlers) - if err != nil { - return fmt.Errorf(`error occurred adding event handlers: %w`, err) + e := core.NewEvent(core.Created, service) + w.synchronizer.AddEvent(e) } - - return nil } -// notControlPlaneNode retrieves the IP Addresses of the nodes in the cluster. Currently, the master node is excluded. This is -// because the master node may or may not be a worker node and thus may not be able to route traffic. -func (w *Watcher) retrieveNodeIps() ([]string, error) { - started := time.Now() - logrus.Debug("Watcher::retrieveNodeIps") +// buildServiceEventHandlerForDelete creates a function that is used as an event handler +// for the informer when Delete events are raised. +func (w *Watcher) buildServiceEventHandlerForDelete() func(interface{}) { + slog.Info("Watcher::buildServiceEventHandlerForDelete") + return func(obj interface{}) { + service := obj.(*v1.Service) + if !w.isDesiredService(service) { + return + } - var nodeIps []string + w.register.removeService(service) - nodes, err := w.settings.K8sClient.CoreV1().Nodes().List(w.settings.Context, metav1.ListOptions{}) - if err != nil { - logrus.Errorf(`error occurred retrieving the list of nodes: %v`, err) - return nil, err + e := core.NewEvent(core.Deleted, service) + w.synchronizer.AddEvent(e) } +} - for _, node := range nodes.Items { +// buildServiceEventHandlerForUpdate creates a function that is used as an event handler +// for the informer when Update events are raised. +func (w *Watcher) buildServiceEventHandlerForUpdate() func(interface{}, interface{}) { + slog.Info("Watcher::buildServiceEventHandlerForUpdate") + return func(previous, updated interface{}) { + previousService := previous.(*v1.Service) + service := updated.(*v1.Service) - // this is kind of a broad assumption, should probably make this a configurable option - if w.notControlPlaneNode(node) { - for _, address := range node.Status.Addresses { - if address.Type == v1.NodeInternalIP { - nodeIps = append(nodeIps, address.Address) - } - } + if w.isDesiredService(previousService) && !w.isDesiredService(service) { + slog.Info("Watcher::service annotation removed", "serviceName", service.Name) + w.register.removeService(previousService) + e := core.NewEvent(core.Deleted, previousService) + w.synchronizer.AddEvent(e) + return + } + + if !w.isDesiredService(service) { + return } - } - logrus.Debugf("Watcher::retrieveNodeIps duration: %d", time.Since(started).Nanoseconds()) + w.register.addOrUpdateService(service) - return nodeIps, nil + e := core.NewEvent(core.Updated, service) + w.synchronizer.AddEvent(e) + } } -// notControlPlaneNode determines if the node is a master node. -func (w *Watcher) notControlPlaneNode(node v1.Node) bool { - logrus.Debug("Watcher::notControlPlaneNode") +// initializeEventListeners initializes the event listeners for the informer. +func (w *Watcher) initializeEventListeners( + servicesInformer cache.SharedIndexInformer, +) error { + slog.Debug("Watcher::initializeEventListeners") + var err error + + serviceHandlers := cache.ResourceEventHandlerFuncs{ + AddFunc: w.buildServiceEventHandlerForAdd(), + DeleteFunc: w.buildServiceEventHandlerForDelete(), + UpdateFunc: w.buildServiceEventHandlerForUpdate(), + } + + endpointSliceHandlers := cache.ResourceEventHandlerFuncs{ + AddFunc: w.buildEndpointSlicesEventHandlerForAdd(), + DeleteFunc: w.buildEndpointSlicesEventHandlerForDelete(), + UpdateFunc: w.buildEndpointSlicesEventHandlerForUpdate(), + } + + nodeHandlers := cache.ResourceEventHandlerFuncs{ + AddFunc: w.buildNodesEventHandlerForAdd(), + DeleteFunc: w.buildNodesEventHandlerForDelete(), + UpdateFunc: w.buildNodesEventHandlerForUpdate(), + } - _, found := node.Labels["node-role.kubernetes.io/control-plane"] + _, err = servicesInformer.AddEventHandler(serviceHandlers) + if err != nil { + return fmt.Errorf(`error occurred adding service event handlers: %w`, err) + } - return !found + _, err = w.endpointSliceInformer.AddEventHandler(endpointSliceHandlers) + if err != nil { + return fmt.Errorf(`error occurred adding endpoint slice event handlers: %w`, err) + } + + _, err = w.nodesInformer.AddEventHandler(nodeHandlers) + if err != nil { + return fmt.Errorf(`error occurred adding node event handlers: %w`, err) + } + + return nil } diff --git a/internal/observation/watcher_test.go b/internal/observation/watcher_test.go index 36d64ee..b8e8369 100644 --- a/internal/observation/watcher_test.go +++ b/internal/observation/watcher_test.go @@ -6,24 +6,18 @@ package observation import ( - "context" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" - "k8s.io/client-go/kubernetes" "testing" + + "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" + "github.com/stretchr/testify/require" ) -func TestWatcher_MustInitialize(t *testing.T) { - watcher, _ := buildWatcher() - if err := watcher.Watch(); err == nil { - t.Errorf("Expected error, got %s", err) - } +func TestWatcher_ErrWithNilInformers(t *testing.T) { + t.Parallel() + _, err := buildWatcherWithNilInformer() + require.Error(t, err, "expected construction of watcher with nil informer to fail") } -func buildWatcher() (*Watcher, error) { - k8sClient := &kubernetes.Clientset{} - settings, _ := configuration.NewSettings(context.Background(), k8sClient) - handler := &mocks.MockHandler{} - - return NewWatcher(settings, handler) +func buildWatcherWithNilInformer() (*Watcher, error) { + return NewWatcher(configuration.Settings{}, nil, nil, nil, nil) } diff --git a/internal/probation/check_test.go b/internal/probation/check_test.go index 208c9a4..95358e5 100644 --- a/internal/probation/check_test.go +++ b/internal/probation/check_test.go @@ -8,6 +8,7 @@ package probation import "testing" func TestCheck_LiveCheck(t *testing.T) { + t.Parallel() check := LiveCheck{} if !check.Check() { t.Errorf("LiveCheck should return true") @@ -15,6 +16,7 @@ func TestCheck_LiveCheck(t *testing.T) { } func TestCheck_ReadyCheck(t *testing.T) { + t.Parallel() check := ReadyCheck{} if !check.Check() { t.Errorf("ReadyCheck should return true") @@ -22,6 +24,7 @@ func TestCheck_ReadyCheck(t *testing.T) { } func TestCheck_StartupCheck(t *testing.T) { + t.Parallel() check := StartupCheck{} if !check.Check() { t.Errorf("StartupCheck should return true") diff --git a/internal/probation/server.go b/internal/probation/server.go index 12b1699..c3328c7 100644 --- a/internal/probation/server.go +++ b/internal/probation/server.go @@ -7,8 +7,10 @@ package probation import ( "fmt" - "github.com/sirupsen/logrus" + "log/slog" + "net" "net/http" + "time" ) const ( @@ -25,7 +27,6 @@ const ( // HealthServer is a server that spins up endpoints for the various k8s health checks. type HealthServer struct { - // The underlying HTTP server. httpServer *http.Server @@ -50,7 +51,7 @@ func NewHealthServer() *HealthServer { // Start spins up the health server. func (hs *HealthServer) Start() { - logrus.Debugf("Starting probe listener on port %d", ListenPort) + slog.Debug("Starting probe listener", "port", ListenPort) address := fmt.Sprintf(":%d", ListenPort) @@ -58,21 +59,28 @@ func (hs *HealthServer) Start() { mux.HandleFunc("/livez", hs.HandleLive) mux.HandleFunc("/readyz", hs.HandleReady) mux.HandleFunc("/startupz", hs.HandleStartup) - hs.httpServer = &http.Server{Addr: address, Handler: mux} + + listener, err := net.Listen("tcp", address) + if err != nil { + slog.Error("failed to listen", "error", err) + return + } + + hs.httpServer = &http.Server{Handler: mux, ReadTimeout: 2 * time.Second} go func() { - if err := hs.httpServer.ListenAndServe(); err != nil { - logrus.Errorf("unable to start probe listener on %s: %v", hs.httpServer.Addr, err) + if err := hs.httpServer.Serve(listener); err != nil { + slog.Error("unable to start probe listener", "address", hs.httpServer.Addr, "error", err) } }() - logrus.Info("Started probe listener on", hs.httpServer.Addr) + slog.Info("Started probe listener", "address", hs.httpServer.Addr) } // Stop shuts down the health server. func (hs *HealthServer) Stop() { if err := hs.httpServer.Close(); err != nil { - logrus.Errorf("unable to stop probe listener on %s: %v", hs.httpServer.Addr, err) + slog.Error("unable to stop probe listener", "address", hs.httpServer.Addr, "error", err) } } @@ -97,14 +105,14 @@ func (hs *HealthServer) handleProbe(writer http.ResponseWriter, _ *http.Request, writer.WriteHeader(http.StatusOK) if _, err := fmt.Fprint(writer, Ok); err != nil { - logrus.Error(err) + slog.Error(err.Error()) } } else { writer.WriteHeader(http.StatusServiceUnavailable) if _, err := fmt.Fprint(writer, ServiceNotAvailable); err != nil { - logrus.Error(err) + slog.Error(err.Error()) } } } diff --git a/internal/probation/server_test.go b/internal/probation/server_test.go index f594bff..981aa3b 100644 --- a/internal/probation/server_test.go +++ b/internal/probation/server_test.go @@ -6,13 +6,15 @@ package probation import ( - "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" - "github.com/sirupsen/logrus" + "log/slog" "net/http" "testing" + + "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" ) func TestHealthServer_HandleLive(t *testing.T) { + t.Parallel() server := NewHealthServer() writer := mocks.NewMockResponseWriter() server.HandleLive(writer, nil) @@ -23,6 +25,7 @@ func TestHealthServer_HandleLive(t *testing.T) { } func TestHealthServer_HandleReady(t *testing.T) { + t.Parallel() server := NewHealthServer() writer := mocks.NewMockResponseWriter() server.HandleReady(writer, nil) @@ -33,6 +36,7 @@ func TestHealthServer_HandleReady(t *testing.T) { } func TestHealthServer_HandleStartup(t *testing.T) { + t.Parallel() server := NewHealthServer() writer := mocks.NewMockResponseWriter() server.HandleStartup(writer, nil) @@ -43,6 +47,7 @@ func TestHealthServer_HandleStartup(t *testing.T) { } func TestHealthServer_HandleFailCheck(t *testing.T) { + t.Parallel() failCheck := mocks.NewMockCheck(false) server := NewHealthServer() writer := mocks.NewMockResponseWriter() @@ -55,6 +60,7 @@ func TestHealthServer_HandleFailCheck(t *testing.T) { } func TestHealthServer_Start(t *testing.T) { + t.Parallel() server := NewHealthServer() server.Start() @@ -64,10 +70,11 @@ func TestHealthServer_Start(t *testing.T) { if err != nil { t.Error(err) } + defer response.Body.Close() if response.StatusCode != http.StatusOK { t.Errorf("Expected status code %v, got %v", http.StatusAccepted, response.StatusCode) } - logrus.Infof("received a response from the probe server: %v", response) + slog.Info("received a response from the probe server", "response", response) } diff --git a/internal/synchronization/cache.go b/internal/synchronization/cache.go new file mode 100644 index 0000000..14effb9 --- /dev/null +++ b/internal/synchronization/cache.go @@ -0,0 +1,49 @@ +package synchronization + +import ( + "sync" + "time" + + v1 "k8s.io/api/core/v1" +) + +// cache contains the most recent definitions for services monitored by NLK. +// We need these so that if a service is deleted from the shared informer cache, the +// caller can access the spec of the deleted service for cleanup. +type cache struct { + mu sync.RWMutex + store map[ServiceKey]service +} + +type service struct { + service *v1.Service + // removedAt indicates when the service was removed from NGINXaaS + // monitoring. A zero time indicates that the service is still actively + // being monitored by NGINXaaS. + removedAt time.Time +} + +func newCache() *cache { + return &cache{ + store: make(map[ServiceKey]service), + } +} + +func (s *cache) get(key ServiceKey) (service, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + svc, ok := s.store[key] + return svc, ok +} + +func (s *cache) add(key ServiceKey, service service) { + s.mu.Lock() + defer s.mu.Unlock() + s.store[key] = service +} + +func (s *cache) delete(key ServiceKey) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.store, key) +} diff --git a/internal/synchronization/rand.go b/internal/synchronization/rand.go deleted file mode 100644 index 425b99a..0000000 --- a/internal/synchronization/rand.go +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package synchronization - -import ( - "math/rand" - "time" -) - -// charset contains all characters that can be used in random string generation -var charset = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -// number contains all numbers that can be used in random string generation -var number = []byte("0123456789") - -// alphaNumeric contains all characters and numbers that can be used in random string generation -var alphaNumeric = append(charset, number...) - -// RandomString where n is the length of random string we want to generate -func RandomString(n int) string { - b := make([]byte, n) - for i := range b { - // randomly select 1 character from given charset - b[i] = alphaNumeric[rand.Intn(len(alphaNumeric))] - } - return string(b) -} - -// RandomMilliseconds returns a random duration between min and max milliseconds -func RandomMilliseconds(min, max int) time.Duration { - randomizer := rand.New(rand.NewSource(time.Now().UnixNano())) - random := randomizer.Intn(max-min) + min - - return time.Millisecond * time.Duration(random) -} diff --git a/internal/synchronization/synchronizer.go b/internal/synchronization/synchronizer.go index 1198abb..8f0fff6 100644 --- a/internal/synchronization/synchronizer.go +++ b/internal/synchronization/synchronizer.go @@ -6,96 +6,123 @@ package synchronization import ( + "context" + "errors" "fmt" + "log/slog" + "net/http" + "time" + + nginxClient "github.com/nginx/nginx-plus-go-client/v2/client" "github.com/nginxinc/kubernetes-nginx-ingress/internal/application" "github.com/nginxinc/kubernetes-nginx-ingress/internal/communication" "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - nginxClient "github.com/nginxinc/nginx-plus-go-client/v2/client" - "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/wait" + corelisters "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/util/workqueue" ) // Interface defines the interface needed to implement a synchronizer. type Interface interface { - - // AddEvents adds a list of events to the queue. - AddEvents(events core.ServerUpdateEvents) - // AddEvent adds an event to the queue. - AddEvent(event *core.ServerUpdateEvent) + AddEvent(event core.Event) // Run starts the synchronizer. - Run(stopCh <-chan struct{}) + Run(ctx context.Context) error // ShutDown shuts down the synchronizer. ShutDown() } +// StatusError is a wrapper for errors from the go plus client that contain http +// status codes. +type StatusError interface { + Status() int + Code() string +} + +type Translator interface { + Translate(*core.Event) (core.ServerUpdateEvents, error) +} + +type ServiceKey struct { + Name string + Namespace string +} + // Synchronizer is responsible for synchronizing the state of the Border Servers. -// Operating against the "nlk-synchronizer", it handles events by creating a Border Client as specified in the -// Service annotation for the Upstream. see application/border_client.go and application/application_constants.go for details. +// Operating against the "nlk-synchronizer", it handles events by creating +// a Border Client as specified in the Service annotation for the Upstream. +// See application/border_client.go and application/application_constants.go for details. type Synchronizer struct { - eventQueue workqueue.RateLimitingInterface - settings *configuration.Settings + eventQueue workqueue.TypedRateLimitingInterface[ServiceKey] + settings configuration.Settings + translator Translator + cache *cache + serviceLister corelisters.ServiceLister } // NewSynchronizer creates a new Synchronizer. -func NewSynchronizer(settings *configuration.Settings, eventQueue workqueue.RateLimitingInterface) (*Synchronizer, error) { +func NewSynchronizer( + settings configuration.Settings, + eventQueue workqueue.TypedRateLimitingInterface[ServiceKey], + translator Translator, + serviceLister corelisters.ServiceLister, +) (*Synchronizer, error) { synchronizer := Synchronizer{ - eventQueue: eventQueue, - settings: settings, + eventQueue: eventQueue, + settings: settings, + cache: newCache(), + translator: translator, + serviceLister: serviceLister, } return &synchronizer, nil } -// AddEvents adds a list of events to the queue. If no hosts are specified this is a null operation. -// Events will fan out to the number of hosts specified before being added to the queue. -func (s *Synchronizer) AddEvents(events core.ServerUpdateEvents) { - logrus.Debugf(`Synchronizer::AddEvents adding %d events`, len(events)) +// AddEvent adds an event to the rate-limited queue. If no hosts are specified this is a null operation. +func (s *Synchronizer) AddEvent(event core.Event) { + slog.Debug(`Synchronizer::AddEvent`) if len(s.settings.NginxPlusHosts) == 0 { - logrus.Warnf(`No Nginx Plus hosts were specified. Skipping synchronization.`) + slog.Warn(`No Nginx Plus hosts were specified. Skipping synchronization.`) return } - updatedEvents := s.fanOutEventToHosts(events) - - for _, event := range updatedEvents { - s.AddEvent(event) - } -} - -// AddEvent adds an event to the queue. If no hosts are specified this is a null operation. -// Events will be added to the queue after a random delay between MinMillisecondsJitter and MaxMillisecondsJitter. -func (s *Synchronizer) AddEvent(event *core.ServerUpdateEvent) { - logrus.Debugf(`Synchronizer::AddEvent: %#v`, event) - - if event.NginxHost == `` { - logrus.Warnf(`Nginx host was not specified. Skipping synchronization.`) - return + key := ServiceKey{Name: event.Service.Name, Namespace: event.Service.Namespace} + var deletedAt time.Time + if event.Type == core.Deleted { + deletedAt = time.Now() } - after := RandomMilliseconds(s.settings.Synchronizer.MinMillisecondsJitter, s.settings.Synchronizer.MaxMillisecondsJitter) - s.eventQueue.AddAfter(event, after) + s.cache.add(key, service{event.Service, deletedAt}) + s.eventQueue.AddRateLimited(key) } // Run starts the Synchronizer, spins up Goroutines to process events, and waits for a stop signal. -func (s *Synchronizer) Run(stopCh <-chan struct{}) { - logrus.Debug(`Synchronizer::Run`) +func (s *Synchronizer) Run(ctx context.Context) error { + slog.Debug(`Synchronizer::Run`) + + // worker is the main message loop + worker := func() { + slog.Debug(`Synchronizer::worker`) + for s.handleNextServiceEvent(ctx) { + } + } for i := 0; i < s.settings.Synchronizer.Threads; i++ { - go wait.Until(s.worker, 0, stopCh) + go wait.Until(worker, 0, ctx.Done()) } - <-stopCh + <-ctx.Done() + return nil } // ShutDown stops the Synchronizer and shuts down the event queue func (s *Synchronizer) ShutDown() { - logrus.Debugf(`Synchronizer::ShutDown`) + slog.Debug(`Synchronizer::ShutDown`) s.eventQueue.ShutDownWithDrain() } @@ -103,18 +130,15 @@ func (s *Synchronizer) ShutDown() { // NOTE: There is an open issue (https://github.com/nginxinc/nginx-loadbalancer-kubernetes/issues/36) to move creation // of the underlying Border Server client to the NewBorderClient function. func (s *Synchronizer) buildBorderClient(event *core.ServerUpdateEvent) (application.Interface, error) { - logrus.Debugf(`Synchronizer::buildBorderClient`) + slog.Debug(`Synchronizer::buildBorderClient`) var err error - - httpClient, err := communication.NewHttpClient(s.settings) + httpClient, err := communication.NewHTTPClient(s.settings.APIKey, s.settings.SkipVerifyTLS) if err != nil { return nil, fmt.Errorf(`error creating HTTP client: %v`, err) } - opts := nginxClient.WithHTTPClient(httpClient) - - ngxClient, err := nginxClient.NewNginxClient(event.NginxHost, opts) + ngxClient, err := nginxClient.NewNginxClient(event.NginxHost, nginxClient.WithHTTPClient(httpClient)) if err != nil { return nil, fmt.Errorf(`error creating Nginx Plus client: %v`, err) } @@ -124,14 +148,13 @@ func (s *Synchronizer) buildBorderClient(event *core.ServerUpdateEvent) (applica // fanOutEventToHosts takes a list of events and returns a list of events, one for each Border Server. func (s *Synchronizer) fanOutEventToHosts(event core.ServerUpdateEvents) core.ServerUpdateEvents { - logrus.Debugf(`Synchronizer::fanOutEventToHosts: %#v`, event) + slog.Debug(`Synchronizer::fanOutEventToHosts`) var events core.ServerUpdateEvents - for hidx, host := range s.settings.NginxPlusHosts { - for eidx, event := range event { - id := fmt.Sprintf(`[%d:%d]-[%s]-[%s]-[%s]`, hidx, eidx, RandomString(12), event.UpstreamName, host) - updatedEvent := core.ServerUpdateEventWithIdAndHost(event, id, host) + for _, host := range s.settings.NginxPlusHosts { + for _, event := range event { + updatedEvent := core.ServerUpdateEventWithHost(event, host) events = append(events, updatedEvent) } @@ -140,36 +163,86 @@ func (s *Synchronizer) fanOutEventToHosts(event core.ServerUpdateEvents) core.Se return events } -// handleEvent dispatches an event to the proper handler function. -func (s *Synchronizer) handleEvent(event *core.ServerUpdateEvent) error { - logrus.Debugf(`Synchronizer::handleEvent: Id: %s`, event.Id) - - var err error +// handleServiceEvent gets the latest state for the service from the shared +// informer cache, translates the service event into server update events and +// dispatches these events to the proper handler function. +func (s *Synchronizer) handleServiceEvent(ctx context.Context, key ServiceKey) (err error) { + logger := slog.With("service", key) + logger.Debug(`Synchronizer::handleServiceEvent`) + + // if a service exists in the shared informer cache, we can assume that we need to update it + event := core.Event{Type: core.Updated} + + cachedService, exists := s.cache.get(key) + + namespaceLister := s.serviceLister.Services(key.Namespace) + k8sService, err := namespaceLister.Get(key.Name) + switch { + // the service has been deleted. We need to rely on the local cache to + // gather the last known state of the service so we can delete its + // upstream servers + case err != nil && apierrors.IsNotFound(err): + if !exists { + logger.Warn(`Synchronizer::handleServiceEvent: no information could be gained about service`) + return nil + } + // no matter what type the cached event has, the service no longer exists, so the type is Deleted + event.Type = core.Deleted + event.Service = cachedService.service + case err != nil: + return err + case exists && !cachedService.removedAt.IsZero(): + event.Type = core.Deleted + event.Service = cachedService.service + default: + event.Service = k8sService + } - switch event.Type { - case core.Created: - fallthrough + events, err := s.translator.Translate(&event) + if err != nil { + return err + } - case core.Updated: - err = s.handleCreatedUpdatedEvent(event) + if len(events) == 0 { + slog.Warn("Synchronizer::handleServiceEvent: no events to process") + return nil + } - case core.Deleted: - err = s.handleDeletedEvent(event) + events = s.fanOutEventToHosts(events) + + for _, evt := range events { + switch event.Type { + case core.Created, core.Updated: + if handleErr := s.handleCreatedUpdatedEvent(ctx, evt); handleErr != nil { + err = errors.Join(err, handleErr) + } + case core.Deleted: + if handleErr := s.handleDeletedEvent(ctx, evt); handleErr != nil { + err = errors.Join(err, handleErr) + } + default: + slog.Warn(`Synchronizer::handleServiceEvent: unknown event type`, "type", event.Type) + } + } - default: - logrus.Warnf(`Synchronizer::handleEvent: unknown event type: %d`, event.Type) + if err != nil { + return err } - if err == nil { - logrus.Infof(`Synchronizer::handleEvent: successfully %s the nginx+ host(s) for Upstream: %s: Id(%s)`, event.TypeName(), event.UpstreamName, event.Id) + if event.Type == core.Deleted { + s.cache.delete(ServiceKey{Name: event.Service.Name, Namespace: event.Service.Namespace}) } - return err + slog.Debug( + "Synchronizer::handleServiceEvent: successfully handled the service change", "service", key, + ) + + return nil } // handleCreatedUpdatedEvent handles events of type Created or Updated. -func (s *Synchronizer) handleCreatedUpdatedEvent(serverUpdateEvent *core.ServerUpdateEvent) error { - logrus.Debugf(`Synchronizer::handleCreatedUpdatedEvent: Id: %s`, serverUpdateEvent.Id) +func (s *Synchronizer) handleCreatedUpdatedEvent(ctx context.Context, serverUpdateEvent *core.ServerUpdateEvent) error { + slog.Debug(`Synchronizer::handleCreatedUpdatedEvent`) var err error @@ -178,7 +251,7 @@ func (s *Synchronizer) handleCreatedUpdatedEvent(serverUpdateEvent *core.ServerU return fmt.Errorf(`error occurred creating the border client: %w`, err) } - if err = borderClient.Update(serverUpdateEvent); err != nil { + if err = borderClient.Update(ctx, serverUpdateEvent); err != nil { return fmt.Errorf(`error occurred updating the %s upstream servers: %w`, serverUpdateEvent.ClientType, err) } @@ -186,8 +259,8 @@ func (s *Synchronizer) handleCreatedUpdatedEvent(serverUpdateEvent *core.ServerU } // handleDeletedEvent handles events of type Deleted. -func (s *Synchronizer) handleDeletedEvent(serverUpdateEvent *core.ServerUpdateEvent) error { - logrus.Debugf(`Synchronizer::handleDeletedEvent: Id: %s`, serverUpdateEvent.Id) +func (s *Synchronizer) handleDeletedEvent(ctx context.Context, serverUpdateEvent *core.ServerUpdateEvent) error { + slog.Debug(`Synchronizer::handleDeletedEvent`) var err error @@ -196,50 +269,46 @@ func (s *Synchronizer) handleDeletedEvent(serverUpdateEvent *core.ServerUpdateEv return fmt.Errorf(`error occurred creating the border client: %w`, err) } - if err = borderClient.Delete(serverUpdateEvent); err != nil { + err = borderClient.Update(ctx, serverUpdateEvent) + + var se StatusError + switch { + case err == nil: + return nil + case errors.As(err, &se) && se.Status() == http.StatusNotFound: + // if the user has already removed the upstream from their NGINX + // configuration there is nothing left to do + return nil + default: return fmt.Errorf(`error occurred deleting the %s upstream servers: %w`, serverUpdateEvent.ClientType, err) } - - return nil } -// handleNextEvent pulls an event from the event queue and feeds it to the event handler with retry logic -func (s *Synchronizer) handleNextEvent() bool { - logrus.Debug(`Synchronizer::handleNextEvent`) +// handleNextServiceEvent pulls a service from the event queue and feeds it to +// the service event handler with retry logic +func (s *Synchronizer) handleNextServiceEvent(ctx context.Context) bool { + slog.Debug(`Synchronizer::handleNextServiceEvent`) - evt, quit := s.eventQueue.Get() + svc, quit := s.eventQueue.Get() if quit { return false } - defer s.eventQueue.Done(evt) + defer s.eventQueue.Done(svc) - event := evt.(*core.ServerUpdateEvent) - s.withRetry(s.handleEvent(event), event) + s.withRetry(s.handleServiceEvent(ctx, svc), svc) return true } -// worker is the main message loop -func (s *Synchronizer) worker() { - logrus.Debug(`Synchronizer::worker`) - for s.handleNextEvent() { - } -} - // withRetry handles errors from the event handler and requeues events that fail -func (s *Synchronizer) withRetry(err error, event *core.ServerUpdateEvent) { - logrus.Debug("Synchronizer::withRetry") +func (s *Synchronizer) withRetry(err error, key ServiceKey) { + slog.Debug("Synchronizer::withRetry") if err != nil { // TODO: Add Telemetry - if s.eventQueue.NumRequeues(event) < s.settings.Synchronizer.RetryCount { // TODO: Make this configurable - s.eventQueue.AddRateLimited(event) - logrus.Infof(`Synchronizer::withRetry: requeued event: %s; error: %v`, event.Id, err) - } else { - s.eventQueue.Forget(event) - logrus.Warnf(`Synchronizer::withRetry: event %#v has been dropped due to too many retries`, event) - } + s.eventQueue.AddRateLimited(key) + slog.Info(`Synchronizer::withRetry: requeued service update`, "service", key, "error", err) } else { - s.eventQueue.Forget(event) + s.eventQueue.Forget(key) } // TODO: Add error logging } diff --git a/internal/synchronization/synchronizer_test.go b/internal/synchronization/synchronizer_test.go index ef510b8..ba2253b 100644 --- a/internal/synchronization/synchronizer_test.go +++ b/internal/synchronization/synchronizer_test.go @@ -6,21 +6,30 @@ package synchronization import ( - "context" "fmt" "testing" + "time" "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" "github.com/nginxinc/kubernetes-nginx-ingress/test/mocks" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + corelisters "k8s.io/client-go/listers/core/v1" ) func TestSynchronizer_NewSynchronizer(t *testing.T) { - settings, err := configuration.NewSettings(context.Background(), nil) + t.Parallel() - rateLimiter := &mocks.MockRateLimiter{} + rateLimiter := &mocks.MockRateLimiter[ServiceKey]{} - synchronizer, err := NewSynchronizer(settings, rateLimiter) + synchronizer, err := NewSynchronizer( + configuration.Settings{}, + rateLimiter, + &fakeTranslator{}, + newFakeServicesLister(defaultService()), + ) if err != nil { t.Fatalf(`should have been no error, %v`, err) } @@ -31,18 +40,17 @@ func TestSynchronizer_NewSynchronizer(t *testing.T) { } func TestSynchronizer_AddEventNoHosts(t *testing.T) { + t.Parallel() const expectedEventCount = 0 - event := &core.ServerUpdateEvent{ - Id: "", - NginxHost: "", - Type: 0, - UpstreamName: "", - UpstreamServers: nil, - } - settings, err := configuration.NewSettings(context.Background(), nil) - rateLimiter := &mocks.MockRateLimiter{} - synchronizer, err := NewSynchronizer(settings, rateLimiter) + rateLimiter := &mocks.MockRateLimiter[ServiceKey]{} + + synchronizer, err := NewSynchronizer( + defaultSettings(), + rateLimiter, + &fakeTranslator{}, + newFakeServicesLister(defaultService()), + ) if err != nil { t.Fatalf(`should have been no error, %v`, err) } @@ -53,7 +61,7 @@ func TestSynchronizer_AddEventNoHosts(t *testing.T) { // NOTE: Ideally we have a custom logger that can be mocked to capture the log message // and assert a warning was logged that the NGINX Plus host was not specified. - synchronizer.AddEvent(event) + synchronizer.AddEvent(core.Event{}) actualEventCount := rateLimiter.Len() if actualEventCount != expectedEventCount { t.Fatalf(`expected %v events, got %v`, expectedEventCount, actualEventCount) @@ -61,13 +69,18 @@ func TestSynchronizer_AddEventNoHosts(t *testing.T) { } func TestSynchronizer_AddEventOneHost(t *testing.T) { + t.Parallel() const expectedEventCount = 1 - events := buildEvents(1) - settings, err := configuration.NewSettings(context.Background(), nil) - settings.NginxPlusHosts = []string{"https://localhost:8080"} - rateLimiter := &mocks.MockRateLimiter{} + events := buildServerUpdateEvents(1) + + rateLimiter := &mocks.MockRateLimiter[ServiceKey]{} - synchronizer, err := NewSynchronizer(settings, rateLimiter) + synchronizer, err := NewSynchronizer( + defaultSettings("https://localhost:8080"), + rateLimiter, + &fakeTranslator{events, nil}, + newFakeServicesLister(defaultService()), + ) if err != nil { t.Fatalf(`should have been no error, %v`, err) } @@ -76,7 +89,7 @@ func TestSynchronizer_AddEventOneHost(t *testing.T) { t.Fatal("should have an Synchronizer instance") } - synchronizer.AddEvent(events[0]) + synchronizer.AddEvent(buildServiceUpdateEvent(1)) actualEventCount := rateLimiter.Len() if actualEventCount != expectedEventCount { t.Fatalf(`expected %v events, got %v`, expectedEventCount, actualEventCount) @@ -84,17 +97,23 @@ func TestSynchronizer_AddEventOneHost(t *testing.T) { } func TestSynchronizer_AddEventManyHosts(t *testing.T) { + t.Parallel() const expectedEventCount = 1 - events := buildEvents(1) - settings, err := configuration.NewSettings(context.Background(), nil) - settings.NginxPlusHosts = []string{ + events := buildServerUpdateEvents(1) + hosts := []string{ "https://localhost:8080", "https://localhost:8081", "https://localhost:8082", } - rateLimiter := &mocks.MockRateLimiter{} - synchronizer, err := NewSynchronizer(settings, rateLimiter) + rateLimiter := &mocks.MockRateLimiter[ServiceKey]{} + + synchronizer, err := NewSynchronizer( + defaultSettings(hosts...), + rateLimiter, + &fakeTranslator{events, nil}, + newFakeServicesLister(defaultService()), + ) if err != nil { t.Fatalf(`should have been no error, %v`, err) } @@ -103,7 +122,7 @@ func TestSynchronizer_AddEventManyHosts(t *testing.T) { t.Fatal("should have an Synchronizer instance") } - synchronizer.AddEvent(events[0]) + synchronizer.AddEvent(buildServiceUpdateEvent(1)) actualEventCount := rateLimiter.Len() if actualEventCount != expectedEventCount { t.Fatalf(`expected %v events, got %v`, expectedEventCount, actualEventCount) @@ -111,12 +130,17 @@ func TestSynchronizer_AddEventManyHosts(t *testing.T) { } func TestSynchronizer_AddEventsNoHosts(t *testing.T) { + t.Parallel() const expectedEventCount = 0 - events := buildEvents(4) - settings, err := configuration.NewSettings(context.Background(), nil) - rateLimiter := &mocks.MockRateLimiter{} - - synchronizer, err := NewSynchronizer(settings, rateLimiter) + events := buildServerUpdateEvents(4) + rateLimiter := &mocks.MockRateLimiter[ServiceKey]{} + + synchronizer, err := NewSynchronizer( + defaultSettings(), + rateLimiter, + &fakeTranslator{events, nil}, + newFakeServicesLister(defaultService()), + ) if err != nil { t.Fatalf(`should have been no error, %v`, err) } @@ -127,7 +151,10 @@ func TestSynchronizer_AddEventsNoHosts(t *testing.T) { // NOTE: Ideally we have a custom logger that can be mocked to capture the log message // and assert a warning was logged that the NGINX Plus host was not specified. - synchronizer.AddEvents(events) + for i := 0; i < 4; i++ { + synchronizer.AddEvent(buildServiceUpdateEvent(i)) + } + actualEventCount := rateLimiter.Len() if actualEventCount != expectedEventCount { t.Fatalf(`expected %v events, got %v`, expectedEventCount, actualEventCount) @@ -135,13 +162,17 @@ func TestSynchronizer_AddEventsNoHosts(t *testing.T) { } func TestSynchronizer_AddEventsOneHost(t *testing.T) { + t.Parallel() const expectedEventCount = 4 - events := buildEvents(4) - settings, err := configuration.NewSettings(context.Background(), nil) - settings.NginxPlusHosts = []string{"https://localhost:8080"} - rateLimiter := &mocks.MockRateLimiter{} - - synchronizer, err := NewSynchronizer(settings, rateLimiter) + events := buildServerUpdateEvents(1) + rateLimiter := &mocks.MockRateLimiter[ServiceKey]{} + + synchronizer, err := NewSynchronizer( + defaultSettings("https://localhost:8080"), + rateLimiter, + &fakeTranslator{events, nil}, + newFakeServicesLister(defaultService()), + ) if err != nil { t.Fatalf(`should have been no error, %v`, err) } @@ -150,7 +181,10 @@ func TestSynchronizer_AddEventsOneHost(t *testing.T) { t.Fatal("should have an Synchronizer instance") } - synchronizer.AddEvents(events) + for i := 0; i < 4; i++ { + synchronizer.AddEvent(buildServiceUpdateEvent(i)) + } + actualEventCount := rateLimiter.Len() if actualEventCount != expectedEventCount { t.Fatalf(`expected %v events, got %v`, expectedEventCount, actualEventCount) @@ -158,18 +192,25 @@ func TestSynchronizer_AddEventsOneHost(t *testing.T) { } func TestSynchronizer_AddEventsManyHosts(t *testing.T) { + t.Parallel() const eventCount = 4 - events := buildEvents(eventCount) - rateLimiter := &mocks.MockRateLimiter{} - settings, err := configuration.NewSettings(context.Background(), nil) - settings.NginxPlusHosts = []string{ + events := buildServerUpdateEvents(eventCount) + rateLimiter := &mocks.MockRateLimiter[ServiceKey]{} + + hosts := []string{ "https://localhost:8080", "https://localhost:8081", "https://localhost:8082", } - expectedEventCount := eventCount * len(settings.NginxPlusHosts) - synchronizer, err := NewSynchronizer(settings, rateLimiter) + expectedEventCount := 4 + + synchronizer, err := NewSynchronizer( + defaultSettings(hosts...), + rateLimiter, + &fakeTranslator{events, nil}, + newFakeServicesLister(defaultService()), + ) if err != nil { t.Fatalf(`should have been no error, %v`, err) } @@ -178,18 +219,31 @@ func TestSynchronizer_AddEventsManyHosts(t *testing.T) { t.Fatal("should have an Synchronizer instance") } - synchronizer.AddEvents(events) + for i := 0; i < eventCount; i++ { + synchronizer.AddEvent(buildServiceUpdateEvent(i)) + } + actualEventCount := rateLimiter.Len() if actualEventCount != expectedEventCount { t.Fatalf(`expected %v events, got %v`, expectedEventCount, actualEventCount) } } -func buildEvents(count int) core.ServerUpdateEvents { +func buildServiceUpdateEvent(serviceID int) core.Event { + return core.Event{ + Service: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test-service%d", serviceID), + Namespace: "test-namespace", + }, + }, + } +} + +func buildServerUpdateEvents(count int) core.ServerUpdateEvents { events := make(core.ServerUpdateEvents, count) for i := 0; i < count; i++ { events[i] = &core.ServerUpdateEvent{ - Id: fmt.Sprintf("id-%v", i), NginxHost: "https://localhost:8080", Type: 0, UpstreamName: "", @@ -198,3 +252,70 @@ func buildEvents(count int) core.ServerUpdateEvents { } return events } + +func defaultSettings(nginxHosts ...string) configuration.Settings { + return configuration.Settings{ + NginxPlusHosts: nginxHosts, + Synchronizer: configuration.SynchronizerSettings{ + MaxMillisecondsJitter: 750, + MinMillisecondsJitter: 250, + RetryCount: 5, + Threads: 1, + WorkQueueSettings: configuration.WorkQueueSettings{ + RateLimiterBase: time.Second * 2, + RateLimiterMax: time.Second * 60, + Name: "nlk-synchronizer", + }, + }, + } +} + +type fakeTranslator struct { + events core.ServerUpdateEvents + err error +} + +func (t *fakeTranslator) Translate(event *core.Event) (core.ServerUpdateEvents, error) { + return t.events, t.err +} + +func newFakeServicesLister(list ...*v1.Service) corelisters.ServiceLister { + return &servicesLister{ + list: list, + } +} + +type servicesLister struct { + list []*v1.Service + err error +} + +func (l *servicesLister) List(selector labels.Selector) (ret []*v1.Service, err error) { + return l.list, l.err +} + +func (l *servicesLister) Get(name string) (*v1.Service, error) { + for _, service := range l.list { + if service.Name == name { + return service, nil + } + } + + return nil, nil +} + +func (l *servicesLister) Services(name string) corelisters.ServiceNamespaceLister { + return l +} + +func defaultService() *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-service", + Labels: map[string]string{"kubernetes.io/service-name": "default-service"}, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeNodePort, + }, + } +} diff --git a/internal/translation/translator.go b/internal/translation/translator.go index b2d0e87..8491a66 100644 --- a/internal/translation/translator.go +++ b/internal/translation/translator.go @@ -7,64 +7,201 @@ package translation import ( "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/application" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" + "log/slog" + "strings" + "time" + "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" - "strings" + "k8s.io/apimachinery/pkg/labels" + corelisters "k8s.io/client-go/listers/core/v1" + discoverylisters "k8s.io/client-go/listers/discovery/v1" ) +type Translator struct { + endpointSliceLister discoverylisters.EndpointSliceLister + nodeLister corelisters.NodeLister +} + +func NewTranslator( + endpointSliceLister discoverylisters.EndpointSliceLister, + nodeLister corelisters.NodeLister, +) *Translator { + return &Translator{ + endpointSliceLister: endpointSliceLister, + nodeLister: nodeLister, + } +} + // Translate transforms event data into an intermediate format that can be consumed by the BorderClient implementations // and used to update the Border Servers. -func Translate(event *core.Event) (core.ServerUpdateEvents, error) { - logrus.Debug("Translate::Translate") +func (t *Translator) Translate(event *core.Event) (core.ServerUpdateEvents, error) { + slog.Debug("Translate::Translate") + + return t.buildServerUpdateEvents(event.Service.Spec.Ports, event) +} - portsOfInterest := filterPorts(event.Service.Spec.Ports) +// buildServerUpdateEvents builds a list of ServerUpdateEvents based on the event type +// The NGINX+ Client uses a list of servers for Created and Updated events. +// The client performs reconciliation between the list of servers in the NGINX+ Client call +// and the list of servers in NGINX+. +// The NGINX+ Client uses a single server for Deleted events; +// so the list of servers is broken up into individual events. +func (t *Translator) buildServerUpdateEvents(ports []v1.ServicePort, event *core.Event, +) (events core.ServerUpdateEvents, err error) { + slog.Debug("Translate::buildServerUpdateEvents", "ports", ports) - return buildServerUpdateEvents(portsOfInterest, event) + switch event.Service.Spec.Type { + case v1.ServiceTypeNodePort: + return t.buildNodeIPEvents(ports, event) + case v1.ServiceTypeClusterIP: + return t.buildClusterIPEvents(event) + case v1.ServiceTypeLoadBalancer: + return t.buildLoadBalancerEvents(event) + default: + return events, fmt.Errorf("unsupported service type: %s", event.Service.Spec.Type) + } } -// filterPorts returns a list of ports that have the NlkPrefix in the port name. -func filterPorts(ports []v1.ServicePort) []v1.ServicePort { - var portsOfInterest []v1.ServicePort +type upstream struct { + context string + name string +} - for _, port := range ports { - if strings.HasPrefix(port.Name, configuration.NlkPrefix) { - portsOfInterest = append(portsOfInterest, port) +func (t *Translator) buildLoadBalancerEvents(event *core.Event) (events core.ServerUpdateEvents, err error) { + slog.Debug("Translator::buildLoadBalancerEvents", "ports", event.Service.Spec.Ports) + + addresses := make([]string, 0, len(event.Service.Status.LoadBalancer.Ingress)) + for _, ingress := range event.Service.Status.LoadBalancer.Ingress { + addresses = append(addresses, ingress.IP) + } + + for _, port := range event.Service.Spec.Ports { + context, upstreamName, err := getContextAndUpstreamName(port.Name) + if err != nil { + slog.Info("Translator::buildLoadBalancerEvents: ignoring port", "err", err, "name", port.Name) + continue + } + + upstreamServers := buildUpstreamServers(addresses, port.Port) + + switch event.Type { + case core.Created, core.Updated: + events = append(events, core.NewServerUpdateEvent(event.Type, upstreamName, context, upstreamServers)) + case core.Deleted: + events = append(events, core.NewServerUpdateEvent( + core.Updated, upstreamName, context, nil, + )) + default: + slog.Warn(`Translator::buildLoadBalancerEvents: unknown event type`, "type", event.Type) } } - return portsOfInterest + return events, nil } -// buildServerUpdateEvents builds a list of ServerUpdateEvents based on the event type -// The NGINX+ Client uses a list of servers for Created and Updated events; the client performs reconciliation between -// the list of servers in the NGINX+ Client call and the list of servers in NGINX+. -// The NGINX+ Client uses a single server for Deleted events; so the list of servers is broken up into individual events. -func buildServerUpdateEvents(ports []v1.ServicePort, event *core.Event) (core.ServerUpdateEvents, error) { - logrus.Debugf("Translate::buildServerUpdateEvents(ports=%#v)", ports) +func (t *Translator) buildClusterIPEvents(event *core.Event) (events core.ServerUpdateEvents, err error) { + namespace := event.Service.GetObjectMeta().GetNamespace() + serviceName := event.Service.Name + + logger := slog.With("namespace", namespace, "serviceName", serviceName) + logger.Debug("Translate::buildClusterIPEvents") + + if event.Type == core.Deleted { + for _, port := range event.Service.Spec.Ports { + context, upstreamName, pErr := getContextAndUpstreamName(port.Name) + if pErr != nil { + logger.Info(pErr.Error()) + continue + } + events = append(events, core.NewServerUpdateEvent(core.Updated, upstreamName, context, nil)) + } + return events, nil + } + + lister := t.endpointSliceLister.EndpointSlices(namespace) + selector, err := labels.Parse(fmt.Sprintf("kubernetes.io/service-name=%s", serviceName)) + if err != nil { + logger.Error(`error occurred parsing the selector`, "error", err) + return events, err + } + + list, err := lister.List(selector) + if err != nil { + logger.Error(`error occurred retrieving the list of endpoint slices`, "error", err) + return events, err + } + + upstreams := make(map[upstream][]*core.UpstreamServer) + + for _, endpointSlice := range list { + for _, port := range endpointSlice.Ports { + if port.Name == nil || port.Port == nil { + continue + } + + context, upstreamName, err := getContextAndUpstreamName(*port.Name) + if err != nil { + logger.Info(err.Error()) + continue + } + + u := upstream{ + context: context, + name: upstreamName, + } + servers := upstreams[u] + + for _, endpoint := range endpointSlice.Endpoints { + for _, address := range endpoint.Addresses { + host := fmt.Sprintf("%s:%d", address, *port.Port) + servers = append(servers, core.NewUpstreamServer(host)) + } + } + + upstreams[u] = servers + } + } + + for u, servers := range upstreams { + events = append(events, core.NewServerUpdateEvent(core.Updated, u.name, u.context, servers)) + } + + return events, nil +} + +func (t *Translator) buildNodeIPEvents(ports []v1.ServicePort, event *core.Event, +) (core.ServerUpdateEvents, error) { + slog.Debug("Translate::buildNodeIPEvents", "ports", ports) events := core.ServerUpdateEvents{} for _, port := range ports { - ingressName := fixIngressName(port.Name) - upstreamServers, _ := buildUpstreamServers(event.NodeIps, port) - clientType := getClientType(port.Name, event.Service.Annotations) + context, upstreamName, err := getContextAndUpstreamName(port.Name) + if err != nil { + slog.Info(err.Error()) + continue + } + + addresses, err := t.retrieveNodeIps() + if err != nil { + return nil, err + } + + upstreamServers := buildUpstreamServers(addresses, port.NodePort) switch event.Type { case core.Created: fallthrough case core.Updated: - events = append(events, core.NewServerUpdateEvent(event.Type, ingressName, clientType, upstreamServers)) + events = append(events, core.NewServerUpdateEvent(event.Type, upstreamName, context, upstreamServers)) case core.Deleted: - for _, server := range upstreamServers { - events = append(events, core.NewServerUpdateEvent(event.Type, ingressName, clientType, core.UpstreamServers{server})) - } - + events = append(events, core.NewServerUpdateEvent( + core.Updated, upstreamName, context, nil, + )) default: - logrus.Warnf(`Translator::buildServerUpdateEvents: unknown event type: %d`, event.Type) + slog.Warn(`Translator::buildNodeIPEvents: unknown event type`, "type", event.Type) } } @@ -72,32 +209,73 @@ func buildServerUpdateEvents(ports []v1.ServicePort, event *core.Event) (core.Se return events, nil } -func buildUpstreamServers(nodeIps []string, port v1.ServicePort) (core.UpstreamServers, error) { +func buildUpstreamServers(ipAddresses []string, port int32) core.UpstreamServers { var servers core.UpstreamServers - for _, nodeIp := range nodeIps { - host := fmt.Sprintf("%s:%d", nodeIp, port.NodePort) + for _, ip := range ipAddresses { + host := fmt.Sprintf("%s:%d", ip, port) server := core.NewUpstreamServer(host) servers = append(servers, server) } - return servers, nil + return servers } -// fixIngressName removes the NlkPrefix from the port name -func fixIngressName(name string) string { - return name[4:] +// getContextAndUpstreamName returns the nginx context being supplied by the port (either "http" or "stream") +// and the upstream name. +func getContextAndUpstreamName(portName string) (clientType string, appName string, err error) { + context, upstreamName, found := strings.Cut(portName, "-") + switch { + case !found: + return clientType, appName, + fmt.Errorf("ignoring port %s because it is not in the format [http|stream]-{upstreamName}", portName) + case context != "http" && context != "stream": + return clientType, appName, fmt.Errorf("port name %s does not include \"http\" or \"stream\" context", portName) + default: + return context, upstreamName, nil + } } -// getClientType returns the client type for the port, defaults to ClientTypeNginxHttp if no Annotation is found. -func getClientType(portName string, annotations map[string]string) string { - key := fmt.Sprintf("%s/%s", configuration.PortAnnotationPrefix, portName) - logrus.Infof("getClientType: key=%s", key) - if annotations != nil { - if clientType, ok := annotations[key]; ok { - return clientType +// notMasterNode retrieves the IP Addresses of the nodes in the cluster. Currently, the master node is excluded. This is +// because the master node may or may not be a worker node and thus may not be able to route traffic. +func (t *Translator) retrieveNodeIps() ([]string, error) { + started := time.Now() + slog.Debug("Translator::retrieveNodeIps") + + var nodeIps []string + + nodes, err := t.nodeLister.List(labels.Everything()) + if err != nil { + slog.Error("error occurred retrieving the list of nodes", "error", err) + return nil, err + } + + for _, node := range nodes { + if node == nil { + slog.Error("list contains nil node") + continue + } + + // this is kind of a broad assumption, should probably make this a configurable option + if notMasterNode(*node) { + for _, address := range node.Status.Addresses { + if address.Type == v1.NodeInternalIP { + nodeIps = append(nodeIps, address.Address) + } + } } } - return application.ClientTypeNginxHttp + slog.Debug("Translator::retrieveNodeIps duration", "duration", time.Since(started).Nanoseconds()) + + return nodeIps, nil +} + +// notMasterNode determines if the node is a master node. +func notMasterNode(node v1.Node) bool { + slog.Debug("Translator::notMasterNode") + + _, found := node.Labels["node-role.kubernetes.io/master"] + + return !found } diff --git a/internal/translation/translator_test.go b/internal/translation/translator_test.go index 2acfd34..309a07c 100644 --- a/internal/translation/translator_test.go +++ b/internal/translation/translator_test.go @@ -7,12 +7,18 @@ package translation import ( "fmt" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/configuration" - "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - v1 "k8s.io/api/core/v1" "math/rand" "testing" "time" + + "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" + "github.com/nginxinc/kubernetes-nginx-ingress/pkg/pointer" + v1 "k8s.io/api/core/v1" + discovery "k8s.io/api/discovery/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + corelisters "k8s.io/client-go/listers/core/v1" + discoverylisters "k8s.io/client-go/listers/discovery/v1" ) const ( @@ -20,6 +26,9 @@ const ( ManyNodes = 7 NoNodes = 0 OneNode = 1 + ManyEndpointSlices = 7 + NoEndpointSlices = 0 + OneEndpointSlice = 1 TranslateErrorFormat = "Translate() error = %v" ) @@ -28,125 +37,299 @@ const ( */ func TestCreatedTranslateNoPorts(t *testing.T) { - const expectedEventCount = 0 + t.Parallel() + testcases := map[string]struct{ serviceType v1.ServiceType }{ + "nodePort": {v1.ServiceTypeNodePort}, + "clusterIP": {v1.ServiceTypeClusterIP}, + "loadBalancer": {v1.ServiceTypeLoadBalancer}, + } - service := defaultService() - event := buildCreatedEvent(service, OneNode) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) - } + const expectedEventCount = 0 + + service := defaultService(tc.serviceType) + event := buildCreatedEvent(service, 0) - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + translator := NewTranslator( + NewFakeEndpointSliceLister([]*discovery.EndpointSlice{}, nil), + NewFakeNodeLister([]*v1.Node{}, nil), + ) + + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + }) } } func TestCreatedTranslateNoInterestingPorts(t *testing.T) { - const expectedEventCount = 0 - const portCount = 1 + t.Parallel() + testcases := map[string]struct{ serviceType v1.ServiceType }{ + "nodePort": {v1.ServiceTypeNodePort}, + "clusterIP": {v1.ServiceTypeClusterIP}, + "loadBalancer": {v1.ServiceTypeLoadBalancer}, + } - ports := generateUpdatablePorts(portCount, 0) - service := serviceWithPorts(ports) - event := buildCreatedEvent(service, OneNode) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) - } + const expectedEventCount = 0 + const portCount = 1 - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) - } -} + ports := generateUpdatablePorts(portCount, 0) + service := serviceWithPorts(tc.serviceType, ports) + event := buildCreatedEvent(service, 0) -func TestCreatedTranslateOneInterestingPort(t *testing.T) { - const expectedEventCount = 1 - const portCount = 1 + translator := NewTranslator( + NewFakeEndpointSliceLister([]*discovery.EndpointSlice{}, nil), + NewFakeNodeLister([]*v1.Node{}, nil), + ) - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildCreatedEvent(service, OneNode) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +//nolint:dupl +func TestCreatedTranslateOneInterestingPort(t *testing.T) { + t.Parallel() + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 1, 1), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: 1, + }, } - assertExpectedServerCount(t, OneNode, translatedEvents) -} + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() -func TestCreatedTranslateManyInterestingPorts(t *testing.T) { - const expectedEventCount = 4 - const portCount = 4 + const expectedEventCount = 1 + const portCount = 1 - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildCreatedEvent(service, OneNode) + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildCreatedEvent(service, tc.ingresses) - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, tc.expectedServerCount, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +//nolint:dupl +func TestCreatedTranslateManyInterestingPorts(t *testing.T) { + t.Parallel() + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 4, 4), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: 1, + }, } - assertExpectedServerCount(t, OneNode, translatedEvents) -} + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() -func TestCreatedTranslateManyMixedPorts(t *testing.T) { - const expectedEventCount = 2 - const portCount = 6 - const updatablePortCount = 2 + const expectedEventCount = 4 + const portCount = 4 + + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildCreatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildCreatedEvent(service, OneNode) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + assertExpectedServerCount(t, tc.expectedServerCount, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +//nolint:dupl +func TestCreatedTranslateManyMixedPorts(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 6, 2), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: 1, + }, } - assertExpectedServerCount(t, OneNode, translatedEvents) -} + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() -func TestCreatedTranslateManyMixedPortsAndManyNodes(t *testing.T) { - const expectedEventCount = 2 - const portCount = 6 - const updatablePortCount = 2 + const expectedEventCount = 2 + const portCount = 6 + const updatablePortCount = 2 - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildCreatedEvent(service, ManyNodes) + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildCreatedEvent(service, tc.ingresses) - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, tc.expectedServerCount, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +func TestCreatedTranslateManyMixedPortsAndManyNodes(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(ManyNodes), + expectedServerCount: ManyNodes, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(ManyEndpointSlices, 6, 2), + expectedServerCount: ManyEndpointSlices, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: ManyNodes, + expectedServerCount: ManyNodes, + }, } - assertExpectedServerCount(t, ManyNodes, translatedEvents) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 2 + const portCount = 6 + const updatablePortCount = 2 + + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildCreatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, ManyNodes, translatedEvents) + }) + } } /* @@ -154,125 +337,326 @@ func TestCreatedTranslateManyMixedPortsAndManyNodes(t *testing.T) { */ func TestUpdatedTranslateNoPorts(t *testing.T) { - const expectedEventCount = 0 - - service := defaultService() - event := buildUpdatedEvent(service, OneNode) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 0, 0), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: OneNode, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 0 + + service := defaultService(tc.serviceType) + event := buildUpdatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + }) } } func TestUpdatedTranslateNoInterestingPorts(t *testing.T) { - const expectedEventCount = 0 - const portCount = 1 - - ports := generateUpdatablePorts(portCount, 0) - service := serviceWithPorts(ports) - event := buildUpdatedEvent(service, OneNode) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 1, 0), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: OneNode, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 0 + const portCount = 1 + + ports := generateUpdatablePorts(portCount, 0) + service := serviceWithPorts(tc.serviceType, ports) + event := buildUpdatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + }) } } func TestUpdatedTranslateOneInterestingPort(t *testing.T) { - const expectedEventCount = 1 - const portCount = 1 - - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildUpdatedEvent(service, OneNode) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 1, 1), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: OneNode, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 1 + const portCount = 1 + + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildUpdatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, OneNode, translatedEvents) + }) } - - assertExpectedServerCount(t, OneNode, translatedEvents) } +//nolint:dupl func TestUpdatedTranslateManyInterestingPorts(t *testing.T) { - const expectedEventCount = 4 - const portCount = 4 - - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildUpdatedEvent(service, OneNode) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 4, 4), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: OneNode, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 4 + const portCount = 4 + + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildUpdatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, tc.expectedServerCount, translatedEvents) + }) } - - assertExpectedServerCount(t, OneNode, translatedEvents) } +//nolint:dupl func TestUpdatedTranslateManyMixedPorts(t *testing.T) { - const expectedEventCount = 2 - const portCount = 6 - const updatablePortCount = 2 - - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildUpdatedEvent(service, OneNode) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + expectedServerCount: OneNode, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 6, 2), + expectedServerCount: OneEndpointSlice, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: OneNode, + ingresses: OneNode, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 2 + const portCount = 6 + const updatablePortCount = 2 + + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildUpdatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, tc.expectedServerCount, translatedEvents) + }) } - - assertExpectedServerCount(t, OneNode, translatedEvents) } +//nolint:dupl func TestUpdatedTranslateManyMixedPortsAndManyNodes(t *testing.T) { - const expectedEventCount = 2 - const portCount = 6 - const updatablePortCount = 2 - - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildUpdatedEvent(service, ManyNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + ingresses int + endpoints []*discovery.EndpointSlice + expectedServerCount int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(ManyNodes), + expectedServerCount: ManyNodes, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(ManyEndpointSlices, 6, 2), + expectedServerCount: ManyEndpointSlices, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + expectedServerCount: ManyNodes, + ingresses: ManyNodes, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 2 + const portCount = 6 + const updatablePortCount = 2 + + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildUpdatedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, tc.expectedServerCount, translatedEvents) + }) } - - assertExpectedServerCount(t, ManyNodes, translatedEvents) } /* @@ -280,316 +664,749 @@ func TestUpdatedTranslateManyMixedPortsAndManyNodes(t *testing.T) { */ func TestDeletedTranslateNoPortsAndNoNodes(t *testing.T) { - const expectedEventCount = 0 - - service := defaultService() - event := buildDeletedEvent(service, NoNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) - } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() - assertExpectedServerCount(t, ManyNodes, translatedEvents) -} + const expectedEventCount = 0 -func TestDeletedTranslateNoInterestingPortsAndNoNodes(t *testing.T) { - const expectedEventCount = 0 - const portCount = 1 + service := defaultService(tc.serviceType) + event := buildDeletedEvent(service, tc.ingresses) - ports := generateUpdatablePorts(portCount, 0) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, NoNodes) + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +func TestDeletedTranslateNoInterestingPortsAndNoNodes(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + }, } - assertExpectedServerCount(t, ManyNodes, translatedEvents) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 0 + const portCount = 1 + + ports := generateUpdatablePorts(portCount, 0) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) + } } +//nolint:dupl func TestDeletedTranslateOneInterestingPortAndNoNodes(t *testing.T) { - const expectedEventCount = 0 - const portCount = 1 - - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, NoNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(0, 1, 1), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) - } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() - assertExpectedServerCount(t, ManyNodes, translatedEvents) -} + const expectedEventCount = 1 + const portCount = 1 -func TestDeletedTranslateManyInterestingPortsAndNoNodes(t *testing.T) { - const expectedEventCount = 0 - const portCount = 4 + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, NoNodes) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + assertExpectedServerCount(t, 0, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +//nolint:dupl +func TestDeletedTranslateManyInterestingPortsAndNoNodes(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(0, 4, 4), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + }, } - assertExpectedServerCount(t, ManyNodes, translatedEvents) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const portCount = 4 + const expectedEventCount = 4 + + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) + } } func TestDeletedTranslateManyMixedPortsAndNoNodes(t *testing.T) { - const expectedEventCount = 0 - const portCount = 6 - const updatablePortCount = 2 - - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, NoNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(0, 6, 2), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) - } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() - assertExpectedServerCount(t, ManyNodes, translatedEvents) -} + const portCount = 6 + const updatablePortCount = 2 + const expectedEventCount = 2 -func TestDeletedTranslateNoPortsAndOneNode(t *testing.T) { - const expectedEventCount = 0 + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) - service := defaultService() - event := buildDeletedEvent(service, OneNode) + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +//nolint:dupl +func TestDeletedTranslateNoPortsAndOneNode(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 0, 0), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: OneNode, + }, } - assertExpectedServerCount(t, ManyNodes, translatedEvents) -} + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() -func TestDeletedTranslateNoInterestingPortsAndOneNode(t *testing.T) { - const expectedEventCount = 0 - const portCount = 1 + const expectedEventCount = 0 + + service := defaultService(tc.serviceType) + event := buildDeletedEvent(service, tc.ingresses) - ports := generateUpdatablePorts(portCount, 0) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, OneNode) + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +func TestDeletedTranslateNoInterestingPortsAndOneNode(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 1, 0), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: OneNode, + }, } - assertExpectedServerCount(t, ManyNodes, translatedEvents) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const portCount = 1 + const expectedEventCount = 0 + + ports := generateUpdatablePorts(portCount, 0) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) + } } +//nolint:dupl func TestDeletedTranslateOneInterestingPortAndOneNode(t *testing.T) { - const expectedEventCount = 1 - const portCount = 1 - - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, OneNode) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 1, 1), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: OneNode, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) - } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() - assertExpectedServerCount(t, OneNode, translatedEvents) -} + const portCount = 1 + const expectedEventCount = 1 -func TestDeletedTranslateManyInterestingPortsAndOneNode(t *testing.T) { - const expectedEventCount = 4 - const portCount = 4 + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, OneNode) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + assertExpectedServerCount(t, 0, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +//nolint:dupl +func TestDeletedTranslateManyInterestingPortsAndOneNode(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 4, 4), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: OneNode, + }, } - assertExpectedServerCount(t, OneNode, translatedEvents) -} + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() -func TestDeletedTranslateManyMixedPortsAndOneNode(t *testing.T) { - const expectedEventCount = 2 - const portCount = 6 - const updatablePortCount = 2 + const portCount = 4 + const expectedEventCount = 4 - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, OneNode) + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +func TestDeletedTranslateManyMixedPortsAndOneNode(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(OneNode), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(OneEndpointSlice, 6, 2), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: OneNode, + }, } - assertExpectedServerCount(t, OneNode, translatedEvents) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const portCount = 6 + const updatablePortCount = 2 + const expectedEventCount = 2 + + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) + } } +//nolint:dupl func TestDeletedTranslateNoPortsAndManyNodes(t *testing.T) { - const expectedEventCount = 0 - - service := defaultService() - event := buildDeletedEvent(service, ManyNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(ManyNodes), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(ManyEndpointSlices, 0, 0), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: ManyNodes, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) - } + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const expectedEventCount = 0 - assertExpectedServerCount(t, ManyNodes, translatedEvents) -} + service := defaultService(tc.serviceType) + event := buildDeletedEvent(service, tc.ingresses) -func TestDeletedTranslateNoInterestingPortsAndManyNodes(t *testing.T) { - const portCount = 1 - const updatablePortCount = 0 - const expectedEventCount = updatablePortCount * ManyNodes + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, ManyNodes) + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + assertExpectedServerCount(t, 0, translatedEvents) + }) } +} - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) +func TestDeletedTranslateNoInterestingPortsAndManyNodes(t *testing.T) { + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(ManyNodes), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(ManyEndpointSlices, 1, 0), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: ManyNodes, + }, } - assertExpectedServerCount(t, ManyNodes, translatedEvents) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const portCount = 1 + const updatablePortCount = 0 + const expectedEventCount = updatablePortCount * ManyNodes + + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) + } } +//nolint:dupl func TestDeletedTranslateOneInterestingPortAndManyNodes(t *testing.T) { - const portCount = 1 - const expectedEventCount = portCount * ManyNodes - - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, ManyNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(ManyNodes), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(ManyEndpointSlices, 1, 1), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: ManyNodes, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const portCount = 1 + const expectedEventCount = 1 + + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) } - - assertExpectedServerCount(t, OneNode, translatedEvents) } +//nolint:dupl func TestDeletedTranslateManyInterestingPortsAndManyNodes(t *testing.T) { - const portCount = 4 - const expectedEventCount = portCount * ManyNodes - - ports := generatePorts(portCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, ManyNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(ManyNodes), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(ManyEndpointSlices, 4, 4), + }, + "loadBalancer": { + serviceType: v1.ServiceTypeLoadBalancer, + ingresses: ManyNodes, + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const portCount = 4 + const expectedEventCount = 4 + + ports := generatePorts(portCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) } - - assertExpectedServerCount(t, OneNode, translatedEvents) } func TestDeletedTranslateManyMixedPortsAndManyNodes(t *testing.T) { - const portCount = 6 - const updatablePortCount = 2 - const expectedEventCount = updatablePortCount * ManyNodes - - ports := generateUpdatablePorts(portCount, updatablePortCount) - service := serviceWithPorts(ports) - event := buildDeletedEvent(service, ManyNodes) - - translatedEvents, err := Translate(&event) - if err != nil { - t.Fatalf(TranslateErrorFormat, err) + t.Parallel() + + testcases := map[string]struct { + serviceType v1.ServiceType + nodes []*v1.Node + endpoints []*discovery.EndpointSlice + ingresses int + }{ + "nodePort": { + serviceType: v1.ServiceTypeNodePort, + nodes: generateNodes(ManyNodes), + }, + "clusterIP": { + serviceType: v1.ServiceTypeClusterIP, + endpoints: generateEndpointSlices(ManyEndpointSlices, 6, 2), + }, } - actualEventCount := len(translatedEvents) - if actualEventCount != expectedEventCount { - t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + for name, tc := range testcases { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + const portCount = 6 + const updatablePortCount = 2 + const expectedEventCount = 2 + + ports := generateUpdatablePorts(portCount, updatablePortCount) + service := serviceWithPorts(tc.serviceType, ports) + event := buildDeletedEvent(service, tc.ingresses) + + translator := NewTranslator(NewFakeEndpointSliceLister(tc.endpoints, nil), NewFakeNodeLister(tc.nodes, nil)) + translatedEvents, err := translator.Translate(&event) + if err != nil { + t.Fatalf(TranslateErrorFormat, err) + } + + actualEventCount := len(translatedEvents) + if actualEventCount != expectedEventCount { + t.Fatalf(AssertionFailureFormat, expectedEventCount, actualEventCount) + } + + assertExpectedServerCount(t, 0, translatedEvents) + }) } - - assertExpectedServerCount(t, OneNode, translatedEvents) } func assertExpectedServerCount(t *testing.T, expectedCount int, events core.ServerUpdateEvents) { @@ -601,46 +1418,102 @@ func assertExpectedServerCount(t *testing.T, expectedCount int, events core.Serv } } -func defaultService() *v1.Service { - return &v1.Service{} +func defaultService(serviceType v1.ServiceType) *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-service", + Labels: map[string]string{"kubernetes.io/service-name": "default-service"}, + }, + Spec: v1.ServiceSpec{ + Type: serviceType, + }, + } } -func serviceWithPorts(ports []v1.ServicePort) *v1.Service { +func serviceWithPorts(serviceType v1.ServiceType, ports []v1.ServicePort) *v1.Service { return &v1.Service{ Spec: v1.ServiceSpec{ + Type: serviceType, Ports: ports, }, } } -func buildCreatedEvent(service *v1.Service, nodeCount int) core.Event { - return buildEvent(core.Created, service, nodeCount) +func buildCreatedEvent(service *v1.Service, ingressCount int) core.Event { + return buildEvent(core.Created, service, ingressCount) } -func buildDeletedEvent(service *v1.Service, nodeCount int) core.Event { - return buildEvent(core.Deleted, service, nodeCount) +func buildDeletedEvent(service *v1.Service, ingressCount int) core.Event { + return buildEvent(core.Deleted, service, ingressCount) } -func buildUpdatedEvent(service *v1.Service, nodeCount int) core.Event { - return buildEvent(core.Updated, service, nodeCount) +func buildUpdatedEvent(service *v1.Service, ingressCount int) core.Event { + return buildEvent(core.Updated, service, ingressCount) } -func buildEvent(eventType core.EventType, service *v1.Service, nodeCount int) core.Event { - previousService := defaultService() +func buildEvent(eventType core.EventType, service *v1.Service, ingressCount int) core.Event { + event := core.NewEvent(eventType, service) + event.Service.Name = "default-service" + ingresses := make([]v1.LoadBalancerIngress, 0, ingressCount) + for i := range ingressCount { + ingress := v1.LoadBalancerIngress{IP: fmt.Sprintf("ipAddress%d", i)} + ingresses = append(ingresses, ingress) + } + event.Service.Status.LoadBalancer.Ingress = ingresses + return event +} - nodeIps := generateNodeIps(nodeCount) +func generateNodes(count int) (nodes []*v1.Node) { + for i := 0; i < count; i++ { + nodes = append(nodes, &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("node%d", i), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: fmt.Sprintf("10.0.0.%v", i), + }, + }, + }, + }) + } - return core.NewEvent(eventType, service, previousService, nodeIps) + return nodes } -func generateNodeIps(count int) []string { - var nodeIps []string +func generateEndpointSlices(endpointCount, portCount, updatablePortCount int, +) (endpointSlices []*discovery.EndpointSlice) { + servicePorts := generateUpdatablePorts(portCount, updatablePortCount) - for i := 0; i < count; i++ { - nodeIps = append(nodeIps, fmt.Sprintf("10.0.0.%v", i)) + ports := make([]discovery.EndpointPort, 0, len(servicePorts)) + for _, servicePort := range servicePorts { + ports = append(ports, discovery.EndpointPort{ + Name: pointer.To(servicePort.Name), + Port: pointer.To(int32(8080)), + }) + } + + var endpoints []discovery.Endpoint + for i := 0; i < endpointCount; i++ { + endpoints = append(endpoints, discovery.Endpoint{ + Addresses: []string{ + fmt.Sprintf("10.0.0.%v", i), + }, + }) } - return nodeIps + endpointSlices = append(endpointSlices, &discovery.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: "endpointSlice", + Labels: map[string]string{"kubernetes.io/service-name": "default-service"}, + }, + Endpoints: endpoints, + Ports: ports, + }) + + return endpointSlices } func generatePorts(portCount int) []v1.ServicePort { @@ -649,20 +1522,24 @@ func generatePorts(portCount int) []v1.ServicePort { // This is probably A Little Bit of Too Muchâ„¢, but helps to ensure ordering is not a factor. func generateUpdatablePorts(portCount int, updatableCount int) []v1.ServicePort { - var ports []v1.ServicePort + ports := []v1.ServicePort{} updatable := make([]string, updatableCount) nonupdatable := make([]string, portCount-updatableCount) + contexts := []string{"http-", "stream-"} for i := range updatable { - updatable[i] = configuration.NlkPrefix + randomIndex := int(rand.Float32() * 2.0) + updatable[i] = contexts[randomIndex] } for j := range nonupdatable { nonupdatable[j] = "olm-" } - prefixes := append(updatable, nonupdatable...) + var prefixes []string + prefixes = append(prefixes, updatable...) + prefixes = append(prefixes, nonupdatable...) source := rand.NewSource(time.Now().UnixNano()) random := rand.New(source) @@ -670,9 +1547,54 @@ func generateUpdatablePorts(portCount int, updatableCount int) []v1.ServicePort for i, prefix := range prefixes { ports = append(ports, v1.ServicePort{ - Name: fmt.Sprintf("%sport-%d", prefix, i), + Name: fmt.Sprintf("%supstream%d", prefix, i), }) } return ports } + +func NewFakeEndpointSliceLister(list []*discovery.EndpointSlice, err error) discoverylisters.EndpointSliceLister { + return &endpointSliceLister{ + list: list, + err: err, + } +} + +func NewFakeNodeLister(list []*v1.Node, err error) corelisters.NodeLister { + return &nodeLister{ + list: list, + err: err, + } +} + +type nodeLister struct { + list []*v1.Node + err error +} + +func (l *nodeLister) List(selector labels.Selector) (ret []*v1.Node, err error) { + return l.list, l.err +} + +// currently unused +func (l *nodeLister) Get(name string) (*v1.Node, error) { + return nil, nil +} + +type endpointSliceLister struct { + list []*discovery.EndpointSlice + err error +} + +func (l *endpointSliceLister) List(selector labels.Selector) (ret []*discovery.EndpointSlice, err error) { + return l.list, l.err +} + +func (l *endpointSliceLister) Get(name string) (*discovery.EndpointSlice, error) { + return nil, nil +} + +func (l *endpointSliceLister) EndpointSlices(name string) discoverylisters.EndpointSliceNamespaceLister { + return l +} diff --git a/pkg/buildinfo/buildinfo.go b/pkg/buildinfo/buildinfo.go new file mode 100644 index 0000000..5d8839d --- /dev/null +++ b/pkg/buildinfo/buildinfo.go @@ -0,0 +1,15 @@ +package buildinfo + +var semVer string + +// SemVer is the version number of this build as provided by build pipeline +func SemVer() string { + return semVer +} + +var shortHash string + +// ShortHash is the 8 char git shorthash +func ShortHash() string { + return shortHash +} diff --git a/pkg/pointer/pointer.go b/pkg/pointer/pointer.go new file mode 100644 index 0000000..08ff667 --- /dev/null +++ b/pkg/pointer/pointer.go @@ -0,0 +1,56 @@ +// Package pointer provides utilities that assist in working with pointers. +package pointer + +// To returns a pointer to the given value +func To[T any](v T) *T { return &v } + +// From dereferences the pointer if it is not nil or returns d +func From[T any](p *T, d T) T { + if p != nil { + return *p + } + return d +} + +// ToSlice returns a slice of pointers to the given values. +func ToSlice[T any](values []T) []*T { + if len(values) == 0 { + return nil + } + ret := make([]*T, 0, len(values)) + for _, v := range values { + v := v + ret = append(ret, &v) + } + return ret +} + +// FromSlice returns a slice of values to the given pointers, dropping any nils. +func FromSlice[T any](values []*T) []T { + if len(values) == 0 { + return nil + } + ret := make([]T, 0, len(values)) + for _, v := range values { + if v != nil { + ret = append(ret, *v) + } + } + return ret +} + +// Equal reports if p is a pointer to a value equal to v +func Equal[T comparable](p *T, v T) bool { + if p == nil { + return false + } + return *p == v +} + +// ValueEqual reports if value of pointer referenced by p is equal to value of pointer referenced by q +func ValueEqual[T comparable](p *T, q *T) bool { + if p == nil || q == nil { + return p == q + } + return *p == *q +} diff --git a/pkg/pointer/pointer_test.go b/pkg/pointer/pointer_test.go new file mode 100644 index 0000000..e929e58 --- /dev/null +++ b/pkg/pointer/pointer_test.go @@ -0,0 +1,62 @@ +package pointer_test + +import ( + "testing" + + "github.com/nginxinc/kubernetes-nginx-ingress/pkg/pointer" + "github.com/stretchr/testify/require" +) + +func TestTo(t *testing.T) { + t.Parallel() + + for _, v := range []string{"", "hello"} { + require.Equal(t, v, *pointer.To(v)) + } + for _, v := range []int{0, 123456, -123456} { + require.Equal(t, v, *pointer.To(v)) + } + for _, v := range []int64{0, 123456, -123456} { + require.Equal(t, v, *pointer.To(v)) + } +} + +func TestFrom(t *testing.T) { + t.Parallel() + + sv := "s" + sd := "default" + require.Equal(t, sd, pointer.From(nil, sd)) + require.Equal(t, sv, pointer.From(&sv, sd)) + + iv := 1 + id := 2 + require.Equal(t, id, pointer.From(nil, id)) + require.Equal(t, iv, pointer.From(&iv, id)) + + i64v := int64(1) + i64d := int64(2) + require.Equal(t, i64d, pointer.From(nil, i64d)) + require.Equal(t, i64v, pointer.From(&i64v, i64d)) +} + +func TestToSlice_FromSlice(t *testing.T) { + t.Parallel() + + v := []int{1, 2, 3} + require.Equal(t, v, pointer.FromSlice(pointer.ToSlice(v))) + require.Nil(t, pointer.ToSlice([]string{})) + require.Nil(t, pointer.FromSlice([]*string{})) + require.Equal(t, []string{"A", "B"}, pointer.FromSlice([]*string{pointer.To("A"), nil, pointer.To("B")})) +} + +func TestEqual(t *testing.T) { + t.Parallel() + + require.True(t, pointer.Equal(pointer.To(1), 1)) + require.False(t, pointer.Equal(nil, 1)) + require.False(t, pointer.Equal(pointer.To(1), 2)) + + s := new(struct{}) + require.False(t, pointer.Equal(&s, nil)) +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..c071a19 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -ex + +os="$1" + +if [[ -z $CI_COMMIT_SHORT_SHA ]]; then + CI_COMMIT_SHORT_SHA=$(git rev-parse --short=8 HEAD) +fi +if [[ -z $VERSION ]]; then + VERSION=$(cat version) +fi + +if [ "$os" == "linux" ]; then + export GOOS=linux + export GOARCH=amd64 + export CGO_ENABLED=0 +fi + +mkdir -p "$BUILD_DIR" + +pkg_path="./cmd/nginx-loadbalancer-kubernetes" +BUILDPKG="github.com/nginxinc/kubernetes-nginx-ingress/pkg/buildinfo" + +ldflags=( + # Set the value of the string variable in importpath named name to value. + -X "'$BUILDPKG.semVer=$VERSION'" + -X "'$BUILDPKG.shortHash=$CI_COMMIT_SHORT_SHA'" + -s # Omit the symbol table and debug information. + -w # Omit the DWARF symbol table. + -extldflags "'-fno-PIC'" +) + +go build \ + -v -tags "release osusergo" \ + -ldflags "${ldflags[*]}" \ + -o "${BUILD_DIR}/nginxaas-loadbalancer-kubernetes" \ + "$pkg_path" diff --git a/scripts/cnab.sh b/scripts/cnab.sh new file mode 100755 index 0000000..da6bdb4 --- /dev/null +++ b/scripts/cnab.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +log() { + printf "\033[0;36m${*}\033[0m\n" >&2 +} + +package() { + CMD="az acr login --name nlbmarketplaceacrprod --username ${ARM_CLIENT_ID_ACR} --password ${ARM_CLIENT_SECRET_ACR}" + ${CMD} + CMD="cpa buildbundle -d ${BUNDLE_DIR} --telemetryOptOut" + ${CMD} +} + +validate() { + CMD="cpa verify -d ${BUNDLE_DIR} --telemetryOptOut" + ${CMD} + +} + +set_version() { + VERSION=$(cat version) +} + +update_helm_chart() { + yq -ie '.global.azure.images.nlk.registry = .nlk.image.registry | .global.azure.images.nlk.image = .nlk.image.repository | .global.azure.images.nlk.tag = env(VERSION)' charts/nlk/values.yaml + yq -ie '.version = env(VERSION) | .appVersion = env(VERSION)' charts/nlk/Chart.yaml +} + +update_bundle() { + yq -ie '.version = env(VERSION)' charts/manifest.yaml +} + +check_ci() { + if [[ "$CI" != "true" ]]; then + log "This script should be the run in the CI only." + exit 1 + fi +} + +set_vars() { + BUNDLE_DIR="${CI_PROJECT_DIR}/charts/" +} + +main() { + check_ci + set_vars + set_version + update_helm_chart + update_bundle + local action="$1" + case "$action" in + validate) + validate + ;; + package) + package + ;; + *) + log "Action not supported." + exit 1 + ;; + esac +} + +main "$@" diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..dc8a56a --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -eo pipefail + + +if [ -z "$KUBECONFIG" ]; then + echo "KUBECONFIG is not set." + exit 1 +fi +if [ ! -e "$KUBECONFIG" ]; then + echo "KUBECONFIG does not exist." + exit 1 +fi + +root_dir=$(git rev-parse --show-toplevel) +# shellcheck source=/dev/null +source "${root_dir}/.devops.sh" +devops.backend.docker.set "azure.container-registry-dev" +devops.backend.docker.authenticate + +namespace="nlk" +helm_release_name="release-1" +registry="${DEVOPS_DOCKER_URL}" +repository="nginx-azure-lb/nginxaas-loadbalancer-kubernetes/nginxaas-loadbalancer-kubernetes" +image_tag=$(git rev-parse --short=8 HEAD) + +kubectl create namespace "${namespace}" --dry-run=client -o yaml | kubectl apply -f - +kubectl -n "${namespace}" create secret docker-registry regcred \ + --docker-username="${DEVOPS_DOCKER_USER}" \ + --docker-password="${DEVOPS_DOCKER_PASS}" \ + --docker-server="${DEVOPS_DOCKER_URL}" \ + --dry-run=client -o yaml | kubectl apply -f - + +helm -n "$namespace" upgrade "$helm_release_name" ${root_dir}/charts/nlk/ \ + --set nlk.image.registry="${registry}",nlk.image.repository="${repository}",nlk.image.tag="${image_tag}",nlk.imagePullSecrets[0].name=regcred \ + --install \ + --reuse-values \ + --wait \ + --timeout 2m diff --git a/scripts/docker-login.sh b/scripts/docker-login.sh new file mode 100755 index 0000000..1ae5d2f --- /dev/null +++ b/scripts/docker-login.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -eo pipefail + +rootdir=$(git rev-parse --show-toplevel) +docker_login_file=${rootdir}/.devops-utils/.last-docker-login + +# - perform a new docker login if last login was more than 1h ago +ttl_seconds=$((60 * 60)) + +epoch=$(date +%s) + +if [ -e "$docker_login_file" ] && [ $((epoch - $(cat "$docker_login_file"))) -lt $ttl_seconds ]; then + exit 0 +fi + +# shellcheck disable=1090 +source "${rootdir}/.devops.sh" +devops.docker.login > /dev/null +if [ "$CI" != "true" ]; then + devops.backend.docker.set "azure.container-registry-dev" + devops.docker.login > /dev/null +fi +echo "$epoch" > "$docker_login_file" diff --git a/scripts/docker.sh b/scripts/docker.sh new file mode 100755 index 0000000..cdfcd82 --- /dev/null +++ b/scripts/docker.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -eo pipefail + +ROOT_DIR=$(git rev-parse --show-toplevel) + +build() { + echo "building image: $image" + DOCKER_BUILDKIT=1 docker build --target "$image" \ + --label VERSION="$version" \ + --label COMMIT="${CI_COMMIT_SHORT_SHA}" \ + --label PROJECT_NAME="${CI_PROJECT_NAME}" \ + --tag "${repo}:${CI_COMMIT_REF_SLUG}" \ + --tag "${repo}:${CI_COMMIT_REF_SLUG}-$version" \ + --tag "${repo}:${CI_COMMIT_SHORT_SHA}" \ + --platform "linux/amd64" \ + -f "${ROOT_DIR}/Dockerfile" . +} + +publish() { + docker push "$repo:${CI_COMMIT_REF_SLUG}" + docker push "$repo:${CI_COMMIT_REF_SLUG}-$version" + docker push "$repo:${CI_COMMIT_SHORT_SHA}" + if [[ "$CI_COMMIT_REF_SLUG" == "${CI_DEFAULT_BRANCH}" ]]; then + docker tag "$repo:${CI_COMMIT_SHORT_SHA}" "$repo:latest" + docker tag "$repo:${CI_COMMIT_SHORT_SHA}" "$repo:$version" + docker push "$repo:latest" + docker push "$repo:$version" + fi +} + +init_ci_vars() { + if [ -z "$CI_COMMIT_SHORT_SHA" ]; then + CI_COMMIT_SHORT_SHA=$(git rev-parse --short=8 HEAD) + fi + if [ -z "$CI_PROJECT_NAME" ]; then + CI_PROJECT_NAME=$(basename "$ROOT_DIR") + fi + if [ -z "$CI_COMMIT_REF_SLUG" ]; then + CI_COMMIT_REF_SLUG=$( + git rev-parse --abbrev-ref HEAD | tr "[:upper:]" "[:lower:]" \ + | LANG=en_US.utf8 sed -E -e 's/[^a-zA-Z0-9]/-/g' -e 's/^-+|-+$$//g' \ + | cut -c 1-63 + ) + fi + if [ -z "$CI_DEFAULT_BRANCH" ]; then + CI_DEFAULT_BRANCH="main" + fi +} + +print_help () { + echo "Usage: $(basename "$0") " +} + +parse_args() { + if [[ "$#" -ne 1 ]]; then + print_help + exit 0 + fi + + action="$1" + + valid_actions="(build|publish)" + valid_actions_ptn="^${valid_actions}$" + if ! [[ "$action" =~ $valid_actions_ptn ]]; then + echo "Invalid action. Valid actions: $valid_actions" + print_help + exit 1 + fi +} + +# MAIN +image="nginxaas-loadbalancer-kubernetes" +parse_args "$@" +init_ci_vars + +# shellcheck source=/dev/null +source "${ROOT_DIR}/.devops.sh" +if [ "$CI" != "true" ]; then + devops.backend.docker.set "azure.container-registry-dev" +fi +repo="${DEVOPS_DOCKER_URL}/nginx-azure-lb/${CI_PROJECT_NAME}/$image" +# shellcheck source=/dev/null +# shellcheck disable=SC2153 +version=$(cat version) + +"$action" diff --git a/scripts/publish-helm.sh b/scripts/publish-helm.sh new file mode 100755 index 0000000..71155d9 --- /dev/null +++ b/scripts/publish-helm.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +set -eo pipefail + +ROOT_DIR=$(git rev-parse --show-toplevel) + +publish_helm() { + pkg="nginxaas-loadbalancer-kubernetes-${version}.tgz" + helm package --version "${version}" --app-version "${version}" charts/nlk + helm push "${pkg}" "${repo}" +} + +init_ci_vars() { + if [ -z "$CI_PROJECT_NAME" ]; then + CI_PROJECT_NAME=$(basename "$ROOT_DIR") + fi + if [ -z "$CI_COMMIT_REF_SLUG" ]; then + CI_COMMIT_REF_SLUG=$( + git rev-parse --abbrev-ref HEAD | tr "[:upper:]" "[:lower:]" \ + | LANG=en_US.utf8 sed -E -e 's/[^a-zA-Z0-9]/-/g' -e 's/^-+|-+$$//g' \ + | cut -c 1-63 + ) + fi +} + +# MAIN +init_ci_vars + +# shellcheck source=/dev/null +source "${ROOT_DIR}/.devops.sh" +if [ "$CI" != "true" ]; then + devops.backend.docker.set "azure.container-registry-dev" +fi +repo="oci://${DEVOPS_DOCKER_URL}/nginx-azure-lb/${CI_PROJECT_NAME}/charts/${CI_COMMIT_REF_SLUG}" +# shellcheck source=/dev/null +# shellcheck disable=SC2153 +version=$(cat version) + +publish_helm diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..f8ed556 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash + +set -eo pipefail + +docker-image() { + SRC_PATH="nginx-azure-lb/nginxaas-loadbalancer-kubernetes/nginxaas-loadbalancer-kubernetes" + SRC_IMG="${SRC_REGISTRY}/${SRC_PATH}:main-${SRC_TAG}" + DST_PATH="nginx/nginxaas-loadbalancer-kubernetes" + DST_IMG="${DST_REGISTRY}/${DST_PATH}:${DST_TAG}" + + docker pull "${SRC_IMG}" + docker tag "${SRC_IMG}" "${DST_IMG}" + docker push "${DST_IMG}" +} + +helm-chart() { + SRC_PATH="nginx-azure-lb/nginxaas-loadbalancer-kubernetes/charts/main/nginxaas-loadbalancer-kubernetes" + SRC_CHART="oci://${SRC_REGISTRY}/${SRC_PATH}" + DST_PATH="nginxcharts" + DST_CHART="oci://${DST_REGISTRY}/${DST_PATH}" + + helm pull "${SRC_CHART}" --version "${SRC_TAG}" + helm push nginxaas-loadbalancer-kubernetes-${DST_TAG}.tgz "${DST_CHART}" +} + + +help_text() { + echo "Usage: $(basename $0) " +} + +set_docker_common() { + DOCKERHUB_USERNAME=$(devops.secret.get "kic-dockerhub-creds" | jq -r ".username") + if [[ -z "${DOCKERHUB_USERNAME}" ]]; then + echo "DOCKERHUB_USERNAME needs to be set." + exit 1 + fi + + DOCKERHUB_PASSWORD=$(devops.secret.get "kic-dockerhub-creds" | jq -r ".password") + if [[ -z "${DOCKERHUB_PASSWORD}" ]]; then + echo "DOCKERHUB_PASSWORD needs to be set." + exit 1 + fi + SRC_REGISTRY="${DEVOPS_DOCKER_URL}" + DST_REGISTRY="docker.io" + + # Login to NGINX DevOps Registry. + devops.docker.login + # Login to Dockerhub. + docker login --username "${DOCKERHUB_USERNAME}" --password "${DOCKERHUB_PASSWORD}" "${DST_REGISTRY}" + SRC_TAG=$(echo "${CI_COMMIT_TAG}" | cut -f 2 -d "v") + DST_TAG="${SRC_TAG}" +} + +parse_args() { + if [[ "$#" -ne 1 ]]; then + help_text + exit 0 + fi + + artifact="${1}" + valid_artifact="(docker-image|helm-chart)" + valid_artifact_pttn="^${valid_artifact}$" + if ! [[ "${artifact}" =~ $valid_artifact_pttn ]]; then + echo "Invalid artifact type. Valid artifact types: $valid_artifact" + help_text + exit 1 + fi +} + +main() { + if [[ "${CI}" != "true" ]]; then + echo "This script is meant to be run in the CI." + exit 1 + fi + pttn="^v[0-9]+\.[0-9]+\.[0-9]+" + if ! [[ "${CI_COMMIT_TAG}" =~ $pttn ]]; then + echo "CI_COMMIT_TAG needs to be set to valid semver format." + exit 1 + fi + parse_args "$@" + ROOT_DIR=$(git rev-parse --show-toplevel) + source ${ROOT_DIR}/.devops.sh + set_docker_common + "$artifact" +} + +main "$@" diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..00ca02d --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -ex + +export GO_DATA_RACE=${GO_DATA_RACE:-false} +if [ "$GO_DATA_RACE" == "true" ]; then + go_flags+=("-race") +fi + +outfile="${RESULTS_DIR}/coverage.out" +mkdir -p "$RESULTS_DIR" +go_flags+=("-cover" -coverprofile="$outfile") + +set +e +gotestsum --junitfile "${RESULTS_DIR}/report.xml" --format pkgname -- "${go_flags[@]}" ./... +rc=$? +set -e +echo "Total code coverage:" +go tool cover -func="$outfile" | grep 'total:' | tee "${RESULTS_DIR}/anybadge.out" +go tool cover -html="$outfile" -o "${RESULTS_DIR}/coverage.html" +exit $rc diff --git a/test/mocks/mock_handler.go b/test/mocks/mock_handler.go deleted file mode 100644 index b854db9..0000000 --- a/test/mocks/mock_handler.go +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2023 F5 Inc. All rights reserved. - * Use of this source code is governed by the Apache License that can be found in the LICENSE file. - */ - -package mocks - -import "github.com/nginxinc/kubernetes-nginx-ingress/internal/core" - -type MockHandler struct { -} - -func (h *MockHandler) AddRateLimitedEvent(_ *core.Event) { - -} - -func (h *MockHandler) Initialize() { - -} - -func (h *MockHandler) Run(_ <-chan struct{}) { - -} - -func (h *MockHandler) ShutDown() { - -} diff --git a/test/mocks/mock_nginx_plus_client.go b/test/mocks/mock_nginx_plus_client.go index 147a551..991ab08 100644 --- a/test/mocks/mock_nginx_plus_client.go +++ b/test/mocks/mock_nginx_plus_client.go @@ -8,7 +8,7 @@ package mocks import ( "context" - nginxClient "github.com/nginxinc/nginx-plus-go-client/v2/client" + nginxClient "github.com/nginx/nginx-plus-go-client/v2/client" ) type MockNginxClient struct { @@ -30,7 +30,7 @@ func NewErroringMockClient(err error) *MockNginxClient { } } -func (m MockNginxClient) DeleteStreamServer(ctx context.Context, string, _ string) error { +func (m MockNginxClient) DeleteStreamServer(_ context.Context, _ string, _ string) error { m.CalledFunctions["DeleteStreamServer"] = true if m.Error != nil { @@ -40,7 +40,11 @@ func (m MockNginxClient) DeleteStreamServer(ctx context.Context, string, _ strin return nil } -func (m MockNginxClient) UpdateStreamServers(ctx context.Context, _ string, _ []nginxClient.StreamUpstreamServer) ([]nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, error) { +func (m MockNginxClient) UpdateStreamServers( + _ context.Context, + _ string, + _ []nginxClient.StreamUpstreamServer, +) ([]nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, []nginxClient.StreamUpstreamServer, error) { m.CalledFunctions["UpdateStreamServers"] = true if m.Error != nil { @@ -50,7 +54,7 @@ func (m MockNginxClient) UpdateStreamServers(ctx context.Context, _ string, _ [] return nil, nil, nil, nil } -func (m MockNginxClient) DeleteHTTPServer(ctx context.Context, _ string, _ string) error { +func (m MockNginxClient) DeleteHTTPServer(_ context.Context, _ string, _ string) error { m.CalledFunctions["DeleteHTTPServer"] = true if m.Error != nil { @@ -60,7 +64,11 @@ func (m MockNginxClient) DeleteHTTPServer(ctx context.Context, _ string, _ strin return nil } -func (m MockNginxClient) UpdateHTTPServers(ctx context.Context, _ string, _ []nginxClient.UpstreamServer) ([]nginxClient.UpstreamServer, []nginxClient.UpstreamServer, []nginxClient.UpstreamServer, error) { +func (m MockNginxClient) UpdateHTTPServers( + _ context.Context, + _ string, + _ []nginxClient.UpstreamServer, +) ([]nginxClient.UpstreamServer, []nginxClient.UpstreamServer, []nginxClient.UpstreamServer, error) { m.CalledFunctions["UpdateHTTPServers"] = true if m.Error != nil { diff --git a/test/mocks/mock_ratelimitinginterface.go b/test/mocks/mock_ratelimitinginterface.go index ee3ccd4..d5da3b7 100644 --- a/test/mocks/mock_ratelimitinginterface.go +++ b/test/mocks/mock_ratelimitinginterface.go @@ -7,51 +7,50 @@ package mocks import "time" -type MockRateLimiter struct { - items []interface{} +type MockRateLimiter[T any] struct { + items []T } -func (m *MockRateLimiter) Add(_ interface{}) { +func (m *MockRateLimiter[T]) Add(_ T) { } -func (m *MockRateLimiter) Len() int { +func (m *MockRateLimiter[T]) Len() int { return len(m.items) } -func (m *MockRateLimiter) Get() (item interface{}, shutdown bool) { +func (m *MockRateLimiter[T]) Get() (item T, shutdown bool) { if len(m.items) > 0 { item = m.items[0] m.items = m.items[1:] return item, false } - return nil, false + return item, false } -func (m *MockRateLimiter) Done(_ interface{}) { +func (m *MockRateLimiter[T]) Done(_ T) { } -func (m *MockRateLimiter) ShutDown() { +func (m *MockRateLimiter[T]) ShutDown() { } -func (m *MockRateLimiter) ShutDownWithDrain() { +func (m *MockRateLimiter[T]) ShutDownWithDrain() { } -func (m *MockRateLimiter) ShuttingDown() bool { +func (m *MockRateLimiter[T]) ShuttingDown() bool { return true } -func (m *MockRateLimiter) AddAfter(item interface{}, _ time.Duration) { +func (m *MockRateLimiter[T]) AddAfter(item T, _ time.Duration) { m.items = append(m.items, item) } -func (m *MockRateLimiter) AddRateLimited(item interface{}) { +func (m *MockRateLimiter[T]) AddRateLimited(item T) { m.items = append(m.items, item) } -func (m *MockRateLimiter) Forget(_ interface{}) { - +func (m *MockRateLimiter[T]) Forget(_ T) { } -func (m *MockRateLimiter) NumRequeues(_ interface{}) int { +func (m *MockRateLimiter[T]) NumRequeues(_ T) int { return 0 } diff --git a/version b/version new file mode 100644 index 0000000..6085e94 --- /dev/null +++ b/version @@ -0,0 +1 @@ +1.2.1