├── .dockerignore ├── .github ├── .editorconfig ├── dependabot.yaml └── workflows │ ├── analysis-scorecard.yaml │ ├── artifacts.yaml │ ├── ci.yaml │ ├── helm.yml │ └── release.yaml ├── .gitignore ├── .golangci.yml ├── .licensei.toml ├── CODEOWNERS ├── Dockerfile ├── Dockerfile-refresher ├── LICENSE ├── Makefile ├── PROJECT ├── README.md ├── api ├── common │ ├── docker.go │ ├── secret.go │ ├── secret_test.go │ ├── util.go │ └── util_test.go ├── go.mod ├── go.sum └── v1alpha1 │ ├── doc.go │ ├── groupversion_info.go │ ├── imagepullsecret_helpers.go │ ├── imagepullsecret_helpers_test.go │ ├── imagepullsecret_matchers.go │ ├── imagepullsecret_matchers_test.go │ ├── imagepullsecret_types.go │ ├── set.go │ ├── set_test.go │ └── zz_generated.deepcopy.go ├── cmd ├── controller │ └── main.go └── refresher │ └── main.go ├── config ├── crd │ ├── bases │ │ └── images.banzaicloud.io_imagepullsecrets.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── rbac │ └── role.yaml └── samples │ └── images_v1alpha1_imagepullsecrets.yaml ├── controllers ├── const.go ├── env_setup_test.go ├── imagepullsecret_controller.go ├── imagepullsecret_reconciler.go ├── imps_integration_test.go ├── refresher_controller.go ├── refresher_controller_test.go ├── stringset.go └── stringset_test.go ├── deploy └── charts │ ├── embed.go │ ├── go.mod │ └── imagepullsecrets │ ├── .helmignore │ ├── Chart.yaml │ ├── crds │ └── crds.yaml │ ├── examples │ └── test.yaml │ ├── templates │ ├── _helpers.tpl │ ├── default_imps_cr.yaml │ ├── default_imps_secret.yaml │ ├── deployment.yaml │ ├── poddistruptionbudget.yaml │ ├── rbac.yaml │ └── service.yaml │ └── values.yaml ├── docs └── RELEASE.md ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal ├── cron │ ├── reconcile.go │ └── reconcile_test.go ├── errorhandler │ └── errorhandler.go └── log │ ├── config.go │ ├── config_test.go │ ├── logger.go │ └── standard_logger.go ├── pkg ├── ecr │ ├── manager.go │ ├── manager_test.go │ ├── token.go │ ├── token_test.go │ ├── types.go │ └── types_test.go └── pullsecrets │ ├── docker_config.go │ ├── docker_config_test.go │ ├── error.go │ ├── error_test.go │ ├── provider_ecr.go │ ├── provider_ecr_test.go │ ├── provider_error.go │ ├── provider_error_test.go │ ├── provider_static.go │ └── provider_static_test.go └── scripts ├── download-deps.sh ├── install_envtest.sh └── install_kustomize.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | bin/ 3 | build/ 4 | Dockerfile 5 | Dockerfile* 6 | tmp/ 7 | -------------------------------------------------------------------------------- /.github/.editorconfig: -------------------------------------------------------------------------------- 1 | [{*.yml,*.yaml}] 2 | indent_size = 2 3 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "gomod" 10 | directory: "/api" 11 | schedule: 12 | interval: "daily" 13 | 14 | - package-ecosystem: "docker" 15 | directory: "/" 16 | schedule: 17 | interval: "daily" 18 | 19 | - package-ecosystem: "github-actions" 20 | directory: "/" 21 | schedule: 22 | interval: "daily" 23 | -------------------------------------------------------------------------------- /.github/workflows/analysis-scorecard.yaml: -------------------------------------------------------------------------------- 1 | name: OpenSSF Scorecard 2 | 3 | on: 4 | branch_protection_rule: 5 | push: 6 | branches: [ main ] 7 | schedule: 8 | - cron: '30 0 * * 5' 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | 18 | permissions: 19 | actions: read 20 | contents: read 21 | id-token: write 22 | security-events: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 27 | with: 28 | persist-credentials: false 29 | 30 | - name: Run analysis 31 | uses: ossf/scorecard-action@80e868c13c90f172d68d1f4501dee99e2479f7af # v2.1.3 32 | with: 33 | results_file: results.sarif 34 | results_format: sarif 35 | publish_results: true 36 | 37 | - name: Upload results as artifact 38 | uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 39 | with: 40 | name: OpenSSF Scorecard results 41 | path: results.sarif 42 | retention-days: 5 43 | 44 | - name: Upload results to GitHub Security tab 45 | uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0 46 | with: 47 | sarif_file: results.sarif 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | env: 9 | GO_VERSION: '1.20' 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | checks: 16 | name: Checks 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 25 | with: 26 | go-version: ${{ env.GO_VERSION }} 27 | 28 | - name: Cache licenses 29 | id: cache-licenses 30 | uses: actions/cache@v3 31 | with: 32 | path: .licensei.cache 33 | key: licensei-v1-${{ github.ref_name }}-${{ hashFiles('go.sum') }} 34 | restore-keys: | 35 | licensei-v1-${{ steps.set-git-refname.outputs.git_refname }} 36 | licensei-v1-master 37 | licensei-v1 38 | 39 | - name: Download license information for dependencies 40 | env: 41 | GITHUB_TOKEN: ${{ github.token }} # Note: this is required for licensei auth in steps to avoid rate-limiting. 42 | run: make license-cache 43 | 44 | - name: List license information for dependencies 45 | env: 46 | GITHUB_TOKEN: ${{ github.token }} # Note: this is required for licensei auth in steps to avoid rate-limiting. 47 | run: ./bin/licensei list 48 | 49 | - name: Check dependency licenses 50 | env: 51 | GITHUB_TOKEN: ${{ github.token }} # Note: this is required for licensei auth in steps to avoid rate-limiting. 52 | run: go mod vendor && make license-check 53 | 54 | - name: Run lint 55 | run: make lint 56 | 57 | build: 58 | name: Build 59 | runs-on: ubuntu-latest 60 | 61 | steps: 62 | - name: Checkout repository 63 | uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 64 | 65 | - name: Set up Go 66 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 67 | with: 68 | go-version: ${{ env.GO_VERSION }} 69 | 70 | - name: Run build 71 | run: make build build-refresher 72 | 73 | test: 74 | name: Test 75 | runs-on: ubuntu-latest 76 | 77 | steps: 78 | - name: Checkout repository 79 | uses: actions/checkout@83b7061638ee4956cf7545a6f7efe594e5ad0247 # v3.5.1 80 | 81 | - name: Set up Go 82 | uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 83 | with: 84 | go-version: ${{ env.GO_VERSION }} 85 | 86 | - name: Run tests 87 | run: make test 88 | 89 | artifacts: 90 | name: Artifacts 91 | uses: ./.github/workflows/artifacts.yaml 92 | with: 93 | publish: ${{ github.event_name == 'push' && github.ref_name == 'main' }} 94 | permissions: 95 | contents: read 96 | packages: write 97 | id-token: write 98 | security-events: write 99 | -------------------------------------------------------------------------------- /.github/workflows/helm.yml: -------------------------------------------------------------------------------- 1 | name: Helm charts 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "deploy/charts/v[0-9]+.[0-9]+.[0-9]+" 9 | pull_request: 10 | 11 | env: 12 | HELM_PLUGIN_CHARTMUSEUM_PUSH_VERSION: 0.9.0 13 | HELM_PUSH_REPOSITORY_NAME: chartmuseum 14 | HELM_VERSION: 3.1.1 15 | 16 | jobs: 17 | helm: 18 | name: Helm 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Check out code 23 | uses: actions/checkout@v3 24 | 25 | - uses: azure/setup-helm@v3 26 | with: 27 | version: ${{ env.HELM_VERSION }} 28 | 29 | - name: Add Helm repositories 30 | run: | 31 | helm repo add incubator https://charts.helm.sh/incubator 32 | helm repo add chartmuseum https://kubernetes-charts.banzaicloud.com 33 | helm repo add banzaicloud-stable http://kubernetes-charts.banzaicloud.com/branch/master 34 | helm repo add rimusz https://charts.rimusz.net 35 | 36 | - name: Update Helm repositories 37 | run: | 38 | helm repo update 39 | helm repo list 40 | 41 | - name: Update Helm dependencies 42 | run: | 43 | find -H 'deploy/charts/' \ 44 | -maxdepth 2 \ 45 | -name 'Chart.yaml' \ 46 | -execdir helm dependency update \; 47 | 48 | - name: Lint Helm charts 49 | run: | 50 | find -H 'deploy/charts/.' \ 51 | -maxdepth 2 \ 52 | -name 'Chart.yaml' \ 53 | -printf '%h\n' \ 54 | | xargs helm lint 55 | 56 | - name: Set Git refname 57 | id: set-git-refname 58 | run: | 59 | GIT_REFNAME="$(echo "${{ github.ref }}" | sed -r 's@refs/(heads|pull|tags)/@@g')" 60 | 61 | echo "GIT_REFNAME=${GIT_REFNAME}" 62 | echo "git_refname=${GIT_REFNAME}" >> $GITHUB_OUTPUT 63 | 64 | - name: Set Helm push enabled 65 | id: set-helm-push-enabled 66 | run: | 67 | HELM_PUSH_ENABLED="" 68 | if [ "${{ github.event_name }}" == "push" ] && echo "${{ steps.set-git-refname.outputs.git_refname }}" | grep -E -q "deploy/charts/v[0-9]+\.[0-9]+\.[0-9]+"; then 69 | HELM_PUSH_ENABLED=1 70 | else 71 | printf >&2 "Unstable chart (%s) from %s event, chart will not be pushed" "${{ steps.set-git-refname.outputs.git_refname }}" "${{ github.event_name }}" 72 | fi 73 | 74 | echo "HELM_PUSH_ENABLED=${HELM_PUSH_ENABLED}" 75 | echo "helm_push_enabled=${HELM_PUSH_ENABLED}" >> $GITHUB_OUTPUT 76 | 77 | - if: ${{ steps.set-helm-push-enabled.outputs.helm_push_enabled == 1 }} 78 | name: Set chart name 79 | id: set-chart-name 80 | run: | 81 | CHART_NAME="imagepullsecrets" 82 | 83 | echo "CHART_NAME=${CHART_NAME}" 84 | echo "chart_name=${CHART_NAME}" >> $GITHUB_OUTPUT 85 | 86 | - if: ${{ steps.set-helm-push-enabled.outputs.helm_push_enabled == 1 }} 87 | name: Package Helm chart 88 | id: package-chart 89 | run: | 90 | HELM_PACKAGE_OUTPUT=$(helm package "${{ github.workspace }}/deploy/charts/${{ steps.set-chart-name.outputs.chart_name }}") || exit 1 91 | HELM_PACKAGE_PATH="${HELM_PACKAGE_OUTPUT##"Successfully packaged chart and saved it to: "}" 92 | 93 | echo "HELM_PACKAGE_PATH=${HELM_PACKAGE_PATH}" 94 | echo "helm_package_path=${HELM_PACKAGE_PATH}" >> $GITHUB_OUTPUT 95 | 96 | - if: ${{ steps.set-helm-push-enabled.outputs.helm_push_enabled == 1 }} 97 | name: Check Helm chart version in repository 98 | run: | 99 | CHART_PATH="${{ github.workspace }}/deploy/charts/${{ steps.set-chart-name.outputs.chart_name }}" 100 | EXPECTED_CHART_VERSION="$(echo "${{ steps.set-git-refname.outputs.git_refname }}" | awk -F '/v' '{print $NF}')" || exit 1 101 | ACTUAL_CHART_VERSION="$(awk '/version: [0-9]+\.[0-9]+\.[0-9]+/ {print $2}' "${CHART_PATH}/Chart.yaml")" || exit 1 102 | 103 | if [ "${EXPECTED_CHART_VERSION}" != "${ACTUAL_CHART_VERSION}" ]; then 104 | printf >&2 "chart version mismatches, name: %s, expected version (from tag): %s, actual version (from chart): %s" "${{ steps.set-chart-name.outputs.chart_name }}" "${EXPECTED_CHART_VERSION}" "${ACTUAL_CHART_VERSION}" 105 | exit 1 106 | fi 107 | 108 | if helm search repo "${{ env.HELM_PUSH_REPOSITORY_NAME }}/${{ steps.set-chart-name.outputs.chart_name }}" --version "${ACTUAL_CHART_VERSION}" --output json | jq --exit-status 'length > 0'; then 109 | printf >&2 "chart version already exists in the repository, repository: %s, name: %s, version: %s" "${{ env.HELM_PUSH_REPOSITORY_NAME }}" "${{ steps.set-chart-name.outputs.chart_name }}" "${ACTUAL_CHART_VERSION}" 110 | exit 1 111 | fi 112 | 113 | - if: ${{ steps.set-helm-push-enabled.outputs.helm_push_enabled == 1 }} 114 | name: Install Helm ChartMuseum push plugin 115 | run: helm plugin install "https://github.com/chartmuseum/helm-push.git" --version "${{ env.HELM_PLUGIN_CHARTMUSEUM_PUSH_VERSION }}" 116 | 117 | - if: ${{ steps.set-helm-push-enabled.outputs.helm_push_enabled == 1 }} 118 | name: Push Helm chart 119 | env: 120 | HELM_REPO_PASSWORD: ${{ secrets.HELM_REPO_PASSWORD }} 121 | HELM_REPO_USERNAME: ${{ secrets.HELM_REPO_USERNAME }} 122 | run: helm push "${{ steps.package-chart.outputs.helm_package_path }}" "${{ env.HELM_PUSH_REPOSITORY_NAME }}" 123 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | - "v[0-9]+.[0-9]+.[0-9]+-dev.[0-9]+" 8 | - "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+" 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | artifacts: 15 | name: Artifacts 16 | uses: ./.github/workflows/artifacts.yaml 17 | with: 18 | publish: true 19 | permissions: 20 | contents: read 21 | packages: write 22 | id-token: write 23 | security-events: write 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | 26 | /.licensei.cache 27 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - .gen 4 | skip-files: 5 | - ".*zz_.*\\.go$" 6 | 7 | linters: 8 | enable-all: true 9 | disable: 10 | - lll 11 | - gochecknoinits 12 | - gochecknoglobals 13 | - goconst 14 | - funlen 15 | - gocritic 16 | - godox 17 | - wsl 18 | - gocognit 19 | - gomnd 20 | - godot 21 | - goerr113 22 | - nestif 23 | - testpackage 24 | - nolintlint 25 | - wrapcheck 26 | - varnamelen 27 | - ireturn 28 | - exhaustivestruct 29 | - exhaustruct 30 | - gomoddirectives 31 | - exhaustive 32 | - cyclop 33 | - ifshort 34 | 35 | linters-settings: 36 | golint: 37 | min-confidence: 0.1 38 | gocyclo: 39 | min-complexity: 40 40 | gci: 41 | sections: 42 | - standard 43 | - default 44 | - prefix(github.com/banzaicloud/imps) 45 | goimports: 46 | local-prefixes: github.com/banzaicloud/imps 47 | gocritic: 48 | disabled-checks: 49 | - ifElseChain 50 | 51 | issues: 52 | # mainly because of the operator, but we are using helm chart names 53 | # as package names 54 | exclude: 55 | - underscore in package name 56 | - should not use underscores in package names 57 | 58 | exclude-rules: 59 | # zz_ files are messing up the receiver name 60 | - linters: 61 | - stylecheck 62 | text: "ST1016" 63 | -------------------------------------------------------------------------------- /.licensei.toml: -------------------------------------------------------------------------------- 1 | approved = [ 2 | "mit", 3 | "apache-2.0", 4 | "bsd-3-clause", 5 | "bsd-2-clause", 6 | "mpl-2.0", 7 | ] 8 | 9 | ignored = [ 10 | "github.com/davecgh/go-spew", # ISC license 11 | "github.com/gogo/protobuf", 12 | "google.golang.org/protobuf", 13 | "github.com/ghodss/yaml", 14 | "sigs.k8s.io/yaml", # Forked from above 15 | "gopkg.in/fsnotify.v1", 16 | "gomodules.xyz/jsonpatch/v2", # Apache2 17 | ] 18 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence. 3 | * @martonsereg @waynz0r @gallotamas @nishantapatil3 @shanchunyang0919 @tkircsi @sagikazarmark 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG UID=1000 2 | ARG GID=1000 3 | 4 | FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012 AS xx 5 | 6 | FROM --platform=$BUILDPLATFORM golang:1.20.3-alpine3.16@sha256:29c4e6e307eac79e5db29a261b243f27ffe0563fa1767e8d9a6407657c9a5f08 AS builder 7 | ARG UID 8 | ARG GID 9 | 10 | # Create user and group 11 | RUN addgroup -g ${GID} -S appgroup 12 | RUN adduser -u ${UID} -S appuser -G appgroup 13 | 14 | COPY --from=xx / / 15 | 16 | RUN apk add --update --no-cache ca-certificates make git curl tzdata clang lld 17 | 18 | ARG TARGETPLATFORM 19 | 20 | RUN xx-apk --update --no-cache add musl-dev gcc 21 | 22 | RUN xx-go --wrap 23 | 24 | WORKDIR /usr/local/src/imps 25 | 26 | ARG GOPROXY 27 | 28 | ENV CGO_ENABLED=0 29 | 30 | COPY go.mod go.sum ./ 31 | COPY api/go.mod api/go.sum ./api/ 32 | RUN go mod download 33 | 34 | COPY . . 35 | 36 | RUN go build -o /usr/local/bin/manager ./cmd/controller/ 37 | RUN xx-verify /usr/local/bin/manager 38 | 39 | 40 | FROM redhat/ubi8-micro:8.7@sha256:6a56010de933f172b195a1a575855d37b70a4968be8edb35157f6ca193969ad2 AS ubi8 41 | ARG UID 42 | ARG GID 43 | 44 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 45 | 46 | # RedHat certification requires application license to be in /licenses dir 47 | COPY --from=builder /usr/local/src/imps/LICENSE /licenses/LICENSE 48 | COPY --from=builder /usr/local/bin/manager /manager 49 | 50 | COPY --from=builder /etc/passwd /etc/passwd 51 | COPY --from=builder /etc/group /etc/group 52 | USER ${UID}:${GID} 53 | 54 | ENTRYPOINT ["/manager"] 55 | 56 | 57 | FROM gcr.io/distroless/base-debian11:latest@sha256:e711a716d8b7fe9c4f7bbf1477e8e6b451619fcae0bc94fdf6109d490bf6cea0 AS distroless 58 | ARG UID 59 | ARG GID 60 | 61 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 62 | 63 | # RedHat certification requires application license to be in /licenses dir 64 | COPY --from=builder /usr/local/src/imps/LICENSE /licenses/LICENSE 65 | COPY --from=builder /usr/local/bin/manager /manager 66 | 67 | COPY --from=builder /etc/passwd /etc/passwd 68 | COPY --from=builder /etc/group /etc/group 69 | USER ${UID}:${GID} 70 | 71 | ENTRYPOINT ["/manager"] 72 | -------------------------------------------------------------------------------- /Dockerfile-refresher: -------------------------------------------------------------------------------- 1 | ARG UID=1000 2 | ARG GID=1000 3 | 4 | FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012 AS xx 5 | 6 | FROM --platform=$BUILDPLATFORM golang:1.20.3-alpine3.16@sha256:29c4e6e307eac79e5db29a261b243f27ffe0563fa1767e8d9a6407657c9a5f08 AS builder 7 | ARG UID 8 | ARG GID 9 | 10 | # Create user and group 11 | RUN addgroup -g ${GID} -S appgroup 12 | RUN adduser -u ${UID} -S appuser -G appgroup 13 | 14 | COPY --from=xx / / 15 | 16 | RUN apk add --update --no-cache ca-certificates make git curl tzdata clang lld 17 | 18 | ARG TARGETPLATFORM 19 | 20 | RUN xx-apk --update --no-cache add musl-dev gcc 21 | 22 | RUN xx-go --wrap 23 | 24 | WORKDIR /usr/local/src/imps 25 | 26 | ARG GOPROXY 27 | 28 | ENV CGO_ENABLED=0 29 | 30 | COPY go.mod go.sum ./ 31 | COPY api/go.mod api/go.sum ./api/ 32 | RUN go mod download 33 | 34 | COPY . . 35 | 36 | RUN go build -o /usr/local/bin/manager ./cmd/refresher/ 37 | RUN xx-verify /usr/local/bin/manager 38 | 39 | 40 | FROM redhat/ubi8-micro:8.7@sha256:6a56010de933f172b195a1a575855d37b70a4968be8edb35157f6ca193969ad2 AS ubi8 41 | ARG UID 42 | ARG GID 43 | 44 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 45 | 46 | # RedHat certification requires application license to be in /licenses dir 47 | COPY --from=builder /usr/local/src/imps/LICENSE /licenses/LICENSE 48 | COPY --from=builder /usr/local/bin/manager /manager 49 | 50 | COPY --from=builder /etc/passwd /etc/passwd 51 | COPY --from=builder /etc/group /etc/group 52 | USER ${UID}:${GID} 53 | 54 | ENTRYPOINT ["/manager"] 55 | 56 | 57 | FROM gcr.io/distroless/base-debian11:latest@sha256:e711a716d8b7fe9c4f7bbf1477e8e6b451619fcae0bc94fdf6109d490bf6cea0 AS distroless 58 | ARG UID 59 | ARG GID 60 | 61 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 62 | 63 | # RedHat certification requires application license to be in /licenses dir 64 | COPY --from=builder /usr/local/src/imps/LICENSE /licenses/LICENSE 65 | COPY --from=builder /usr/local/bin/manager /manager 66 | 67 | COPY --from=builder /etc/passwd /etc/passwd 68 | COPY --from=builder /etc/group /etc/group 69 | USER ${UID}:${GID} 70 | 71 | ENTRYPOINT ["/manager"] 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Image URL to use all building/pushing image targets 2 | 3 | RACE_DETECTOR ?= 0 4 | 5 | CHART_NAME = imagepullsecrets 6 | 7 | # Image URL to use all building/pushing image targets 8 | IMG ?= imagepullsecrets:latest 9 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 10 | CRD_OPTIONS ?= "crd" 11 | # TODO: Use this when allowDangerousTypes feature is released to support floats 12 | # CRD_OPTIONS ?= "crd:trivialVersions=true,allowDangerousTypes=true" 13 | LICENSEI_VERSION = 0.7.0 14 | GOLANGCI_VERSION ?= 1.52.2 15 | ENVTEST_K8S_VERSION = 1.26.0 16 | 17 | 18 | 19 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 20 | ifeq (,$(shell go env GOBIN)) 21 | GOBIN=$(shell go env GOPATH)/bin 22 | else 23 | GOBIN=$(shell go env GOBIN) 24 | endif 25 | 26 | SERVICE_NAME=$(shell basename ${CURDIR} ) 27 | REPO_ROOT=$(shell git rev-parse --show-toplevel) 28 | MAIN_PACKAGE ?= ./cmd/controller/ 29 | 30 | COMMIT_HASH ?= $(shell git rev-parse --short HEAD 2>/dev/null) 31 | BUILD_DATE ?= $(shell date +%FT%T%z) 32 | VERSION ?= 0.3.12 33 | LDFLAGS += -X github.com/banzaicloud/imps/internal/version.commitHash=${COMMIT_HASH} 34 | LDFLAGS += -X github.com/banzaicloud/imps/internal/version.buildDate=${BUILD_DATE} 35 | LDFLAGS += -X github.com/banzaicloud/imps/internal/version.version=${VERSION} 36 | 37 | ifeq (${RACE_DETECTOR}, 1) 38 | GOARGS += -race 39 | CGO_ENABLED = 1 40 | endif 41 | export CGO_ENABLED ?= 0 42 | 43 | ifeq (${REMOTE_DEBUGGING}, 1) 44 | # disables inlining and optimisations 45 | GOARGS += -gcflags="all=-N -l" 46 | endif 47 | 48 | .PHONY: all 49 | all: build 50 | 51 | .PHONY: test 52 | test: ensure-tools generate fmt vet manifests ## Run tests 53 | KUBEBUILDER_ASSETS="${REPO_ROOT}/bin/envtest/bin/" go test ${GOARGS} ./... -coverprofile cover.out 54 | 55 | bin/golangci-lint: bin/golangci-lint-${GOLANGCI_VERSION} 56 | @ln -sf golangci-lint-${GOLANGCI_VERSION} bin/golangci-lint 57 | bin/golangci-lint-${GOLANGCI_VERSION}: 58 | @mkdir -p bin 59 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b ./bin/ v${GOLANGCI_VERSION} 60 | @mv bin/golangci-lint $@ 61 | 62 | DISABLED_LINTERS ?= --disable=gci --disable=goimports --disable=gofumpt 63 | .PHONY: lint 64 | lint: bin/golangci-lint ## Run linter 65 | # "unused" linter is a memory hog, but running it separately keeps it contained (probably because of caching) 66 | bin/golangci-lint run --disable=unused -c .golangci.yml --timeout 5m 67 | bin/golangci-lint run -c .golangci.yml --timeout 5m 68 | 69 | .PHONY: lint-fix 70 | lint-fix: bin/golangci-lint ## Run linter & fix 71 | # "unused" linter is a memory hog, but running it separately keeps it contained (probably because of caching) 72 | bin/golangci-lint run --disable=unused -c .golangci.yml --fix 73 | bin/golangci-lint run -c .golangci.yml --fix 74 | 75 | .PHONY: build 76 | build: generate fmt vet binary ## Build the binary 77 | 78 | .PHONY: build-refresher 79 | build-refresher: generate fmt vet binary-refresher ## Build the refresher binary 80 | 81 | .PHONY: binary 82 | binary: ## Build the binary without executing any code generators 83 | go build ${GOARGS} -o bin/${SERVICE_NAME} -ldflags "${LDFLAGS}" ${MAIN_PACKAGE} 84 | 85 | .PHONY: binary-refresher 86 | binary-refresher: ## Build the refresher binary without executing any code generators 87 | go build ${GOARGS} -o bin/${SERVICE_NAME}-refresher -ldflags "${LDFLAGS}" ./cmd/refresher 88 | 89 | .PHONY: run 90 | run: generate fmt vet manifests ## Run against the configured Kubernetes cluster in ~/.kube/config 91 | go run ${GOARGS} ${MAIN_PACKAGE} 92 | 93 | .PHONY: ensure-tools 94 | ensure-tools: 95 | @scripts/download-deps.sh 96 | @scripts/install_kustomize.sh 97 | @scripts/install_envtest.sh ${ENVTEST_K8S_VERSION} 98 | 99 | .PHONY: install 100 | install: ensure-tools manifests ## Install CRDs into a cluster 101 | ${REPO_ROOT}/bin/kustomize build config/crd | kubectl apply -f - 102 | 103 | .PHONY: uninstall 104 | uninstall: ensure-tools manifests ## Uninstall CRDs from a cluster 105 | ${REPO_ROOT}/bin/kustomize build config/crd | kubectl delete -f - 106 | 107 | .PHONY: deploy 108 | deploy: ensure-tools manifests ## Deploy controller in the configured Kubernetes cluster in ~/.kube/config 109 | cd config/manager && ${REPO_ROOT}/bin/kustomize edit set image controller=${IMG} 110 | ${REPO_ROOT}/bin/kustomize build config/default | kubectl apply -f - 111 | 112 | # Generate manifests e.g. CRD, RBAC etc. 113 | .PHONY: manifests 114 | manifests: ensure-tools 115 | ${REPO_ROOT}/bin/controller-gen $(CRD_OPTIONS) rbac:roleName=binary-role object webhook paths="./..." output:crd:artifacts:config=config/crd/bases 116 | 117 | .PHONY: fmt 118 | fmt: ## Run go fmt against code 119 | go fmt ./... 120 | cd deploy/charts; go fmt ./... 121 | 122 | .PHONY: vet 123 | vet: ## Run go vet against code 124 | go vet ./... 125 | cd deploy/charts; go vet ./... 126 | 127 | # Generate code 128 | .PHONY: generate 129 | generate: ensure-tools manifests generate-helm-crds fmt ## Generate manifests, CRDs 130 | 131 | .PHONY: generate-helm-crds 132 | generate-helm-crds: ensure-tools ## Update the CRDs in our helm charts 133 | ${REPO_ROOT}/bin/kustomize build config/crd > deploy/charts/${CHART_NAME}/crds/crds.yaml 134 | 135 | .PHONY: docker-build 136 | docker-build: test ## Build the docker image (to override image name please set IMG) 137 | docker build . --build-arg LDFLAGS="${LDFLAGS}" -f ${CURDIR}/Dockerfile -t ${IMG} 138 | 139 | .PHONY: docker-build-refresher 140 | docker-build-refresher: test ## Build the docker image (to override image name please set IMG) 141 | docker build . --build-arg LDFLAGS="${LDFLAGS}" -f ${CURDIR}/Dockerfile-refresher -t ${IMG} 142 | 143 | .PHONY: docker-push 144 | docker-push: ## Push the docker image (to override image name please set IMG) 145 | docker push ${IMG} 146 | 147 | bin/licensei: bin/licensei-${LICENSEI_VERSION} 148 | @ln -sf licensei-${LICENSEI_VERSION} bin/licensei 149 | bin/licensei-${LICENSEI_VERSION}: 150 | @mkdir -p bin 151 | curl -sfL https://raw.githubusercontent.com/goph/licensei/master/install.sh | bash -s v${LICENSEI_VERSION} 152 | @mv bin/licensei $@ 153 | 154 | .PHONY: license-check 155 | license-check: bin/licensei ## Run license check 156 | bin/licensei check 157 | bin/licensei header 158 | .PHONY: license-cache 159 | license-cache: bin/licensei ## Generate license cache 160 | bin/licensei cache 161 | 162 | MAKEFILE_LIST=Makefile 163 | 164 | .PHONY: help 165 | .DEFAULT_GOAL := help 166 | help: 167 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: banzaicloud.io 2 | repo: github.com/banzaicloud/backyards/services/imp 3 | resources: 4 | - group: images 5 | kind: ImagePullSecret 6 | version: v1alpha1 7 | version: "2" 8 | -------------------------------------------------------------------------------- /api/common/docker.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type LoginCredentials struct { 4 | Username string `json:"username"` 5 | Password string `json:"password"` 6 | Auth string `json:"auth"` // base64 encoded username:password 7 | } 8 | 9 | // DockerRegistryConfig represents a docker compliant image pull secret json file 10 | type DockerRegistryConfig struct { 11 | Auths map[string]LoginCredentials `json:"auths"` 12 | } 13 | -------------------------------------------------------------------------------- /api/common/secret.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package common 16 | 17 | import ( 18 | "encoding/base64" 19 | "encoding/json" 20 | "fmt" 21 | 22 | "emperror.dev/errors" 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | ) 26 | 27 | // nolint:gosec 28 | const ( 29 | SecretTypeBasicAuth = "kubernetes.io/dockerconfigjson" 30 | SecretTypeECRCredentials = "banzaicloud.io/aws-ecr-login-config" 31 | 32 | SecretKeyDockerConfig = ".dockerconfigjson" 33 | 34 | ECRSecretRegion = "region" 35 | ECRSecretAccountID = "accountID" 36 | ECRSecretKeyAccessKeyID = "accessKeyID" 37 | ECRSecretSecretKey = "secretKey" 38 | ECRRoleArn = "roleArn" 39 | ) 40 | 41 | func NewBasicAuthSecret(secretNamespace, secretName, registry, user, password string) (*corev1.Secret, error) { 42 | config := DockerRegistryConfig{ 43 | Auths: map[string]LoginCredentials{ 44 | registry: { 45 | Username: user, 46 | Password: password, 47 | Auth: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, password))), 48 | }, 49 | }, 50 | } 51 | 52 | dockerJSON, err := json.Marshal(config) 53 | if err != nil { 54 | return nil, errors.Wrap(err, "cannot serialize docker configuration") 55 | } 56 | 57 | secret := &corev1.Secret{ 58 | ObjectMeta: metav1.ObjectMeta{ 59 | Name: secretName, 60 | Namespace: secretNamespace, 61 | }, 62 | Type: SecretTypeBasicAuth, 63 | StringData: map[string]string{ 64 | SecretKeyDockerConfig: string(dockerJSON), 65 | }, 66 | } 67 | secret.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) 68 | return secret, nil 69 | } 70 | 71 | func NewECRLoginCredentialsSecret(secretNamespace, secretName, accountID, region, awsAccessKeyID, awsSecretAccessKey string) *corev1.Secret { 72 | secret := &corev1.Secret{ 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Name: secretName, 75 | Namespace: secretNamespace, 76 | }, 77 | Type: SecretTypeECRCredentials, 78 | StringData: map[string]string{ 79 | ECRSecretRegion: region, 80 | ECRSecretAccountID: accountID, 81 | ECRSecretKeyAccessKeyID: awsAccessKeyID, 82 | ECRSecretSecretKey: awsSecretAccessKey, 83 | }, 84 | } 85 | secret.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) 86 | return secret 87 | } 88 | -------------------------------------------------------------------------------- /api/common/secret_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/assert" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestSecret_NewBasicAuthSecret(t *testing.T) { 12 | type args struct { 13 | secretNamespace string 14 | secretName string 15 | registry string 16 | user string 17 | password string 18 | } 19 | 20 | tests := []struct { 21 | name string 22 | args args 23 | want *corev1.Secret 24 | }{ 25 | { 26 | name: "secret generation works as expected", 27 | args: args{ 28 | secretNamespace: "testSecretNamespace", 29 | secretName: "testSecretName", 30 | registry: "test.io", 31 | user: "testUser", 32 | password: "testPassword", 33 | }, 34 | want: &corev1.Secret{ 35 | Type: "kubernetes.io/dockerconfigjson", 36 | TypeMeta: metav1.TypeMeta{ 37 | Kind: "Secret", 38 | APIVersion: "v1", 39 | }, 40 | ObjectMeta: metav1.ObjectMeta{ 41 | Name: "testSecretName", 42 | Namespace: "testSecretNamespace", 43 | }, 44 | StringData: map[string]string{ 45 | ".dockerconfigjson": "{\"auths\":{\"test.io\":{\"username\":\"testUser\",\"password\":\"testPassword\",\"auth\":\"dGVzdFVzZXI6dGVzdFBhc3N3b3Jk\"}}}", 46 | }, 47 | }, 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | found, err := NewBasicAuthSecret(tt.args.secretNamespace, tt.args.secretName, tt.args.registry, tt.args.user, tt.args.password) 53 | 54 | assert.DeepEqual(t, tt.want, found) 55 | assert.NilError(t, err) 56 | }) 57 | } 58 | } 59 | 60 | func TestSecret_NewECRLoginCredentialsSecret(t *testing.T) { 61 | type args struct { 62 | secretNamespace string 63 | secretName string 64 | accountID string 65 | region string 66 | awsAccessKeyID string 67 | awsSecretAccessKey string 68 | } 69 | 70 | tests := []struct { 71 | name string 72 | args args 73 | want *corev1.Secret 74 | }{ 75 | { 76 | name: "secret generation works as expected", 77 | args: args{ 78 | secretNamespace: "testSecretNamespace", 79 | secretName: "testSecretName", 80 | accountID: "testAccountID", 81 | region: "testRegion", 82 | awsAccessKeyID: "testKeyID", 83 | awsSecretAccessKey: "testSecretAccessKey", 84 | }, 85 | want: &corev1.Secret{ 86 | Type: "banzaicloud.io/aws-ecr-login-config", 87 | TypeMeta: metav1.TypeMeta{ 88 | Kind: "Secret", 89 | APIVersion: "v1", 90 | }, 91 | ObjectMeta: metav1.ObjectMeta{ 92 | Name: "testSecretName", 93 | Namespace: "testSecretNamespace", 94 | }, 95 | StringData: map[string]string{ 96 | "region": "testRegion", 97 | "accountID": "testAccountID", 98 | "accessKeyID": "testKeyID", 99 | "secretKey": "testSecretAccessKey", 100 | }, 101 | }, 102 | }, 103 | } 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | found := NewECRLoginCredentialsSecret(tt.args.secretNamespace, tt.args.secretName, tt.args.accountID, tt.args.region, tt.args.awsAccessKeyID, tt.args.awsSecretAccessKey) 107 | 108 | assert.DeepEqual(t, tt.want, found) 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /api/common/util.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package common 16 | 17 | import ( 18 | "fmt" 19 | "hash/crc32" 20 | "regexp" 21 | "strings" 22 | ) 23 | 24 | var invalidCharacterRegexp = regexp.MustCompile("[^a-z0-9.-]+") 25 | 26 | // Total limit is 253 27 | const maxSantiziedLength = 100 28 | 29 | func SecretNameFromURL(prefix, url string) string { 30 | sanitizedName := strings.ToLower(url) 31 | sanitizedName = invalidCharacterRegexp.ReplaceAllString(sanitizedName, "-") 32 | if len(sanitizedName) > maxSantiziedLength { 33 | sanitizedName = sanitizedName[0:maxSantiziedLength] 34 | } 35 | sanitizedName = strings.Trim(sanitizedName, "-") 36 | 37 | urlCRC := crc32.ChecksumIEEE([]byte(url)) 38 | 39 | return fmt.Sprintf("%s-%s-pull-secret-%08x", prefix, sanitizedName, urlCRC) 40 | } 41 | -------------------------------------------------------------------------------- /api/common/util_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/assert" 7 | ) 8 | 9 | func TestUtil_SecretNameFromURL(t *testing.T) { 10 | t.Parallel() 11 | type args struct { 12 | prefix string 13 | url string 14 | } 15 | 16 | tests := []struct { 17 | name string 18 | args args 19 | want string 20 | }{ 21 | { 22 | name: "basic functionality check", 23 | args: args{ 24 | prefix: "test-prefix", 25 | url: "testing.test", 26 | }, 27 | want: "test-prefix-testing.test-pull-secret-4b3d6963", 28 | }, 29 | { 30 | name: "invalid characters in URL", 31 | args: args{ 32 | prefix: "test-prefix", 33 | url: "+test'ing?.test!", 34 | }, 35 | want: "test-prefix-test-ing-.test-pull-secret-fb0323c3", 36 | }, 37 | { 38 | name: "URL is too long", 39 | args: args{ 40 | prefix: "test-prefix", 41 | url: "testing.loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooonoooooooong.test", 42 | }, 43 | want: "test-prefix-testing.loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooonoooooooong.tes-pull-secret-240fd929", 44 | }, 45 | } 46 | for _, tt := range tests { 47 | tt := tt 48 | t.Run(tt.name, func(t *testing.T) { 49 | t.Parallel() 50 | found := SecretNameFromURL(tt.args.prefix, tt.args.url) 51 | 52 | assert.Equal(t, tt.want, found) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /api/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/banzaicloud/imps/api 2 | 3 | go 1.18 4 | 5 | require ( 6 | emperror.dev/errors v0.8.0 7 | gotest.tools v2.2.0+incompatible 8 | k8s.io/api v0.20.11 9 | k8s.io/apimachinery v0.20.11 10 | sigs.k8s.io/controller-runtime v0.6.5 11 | ) 12 | 13 | require ( 14 | github.com/go-logr/logr v0.4.0 // indirect 15 | github.com/gogo/protobuf v1.3.2 // indirect 16 | github.com/google/go-cmp v0.5.5 // indirect 17 | github.com/google/gofuzz v1.1.0 // indirect 18 | github.com/json-iterator/go v1.1.11 // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 20 | github.com/modern-go/reflect2 v1.0.1 // indirect 21 | github.com/pkg/errors v0.9.1 // indirect 22 | github.com/stretchr/testify v1.7.2 // indirect 23 | go.uber.org/atomic v1.7.0 // indirect 24 | go.uber.org/multierr v1.6.0 // indirect 25 | golang.org/x/net v0.7.0 // indirect 26 | golang.org/x/text v0.7.0 // indirect 27 | gopkg.in/inf.v0 v0.9.1 // indirect 28 | gopkg.in/yaml.v2 v2.4.0 // indirect 29 | k8s.io/klog/v2 v2.9.0 // indirect 30 | sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /api/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package v1alpha1 contains API Schema definitions for the images v1alpha1 API group 16 | // +k8s:openapi-gen=true 17 | // +k8s:deepcopy-gen=package,register 18 | // +k8s:conversion-gen=github.com/banzaicloud/imps/api 19 | // +k8s:defaulter-gen=TypeMeta 20 | // +groupName=images.banzaicloud.io 21 | package v1alpha1 22 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package v1alpha1 contains API Schema definitions for the images v1alpha1 API group 16 | // +kubebuilder:object:generate=true 17 | // +groupName=images.banzaicloud.io 18 | package v1alpha1 19 | 20 | import ( 21 | "k8s.io/apimachinery/pkg/runtime/schema" 22 | "sigs.k8s.io/controller-runtime/pkg/scheme" 23 | ) 24 | 25 | var ( 26 | // GroupVersion is group version used to register these objects 27 | GroupVersion = schema.GroupVersion{Group: "images.banzaicloud.io", Version: "v1alpha1"} 28 | 29 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 30 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 31 | 32 | // AddToScheme adds the types in this group-version to the given scheme. 33 | AddToScheme = SchemeBuilder.AddToScheme 34 | ) 35 | -------------------------------------------------------------------------------- /api/v1alpha1/imagepullsecret_helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1alpha1 16 | 17 | import ( 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | "k8s.io/apimachinery/pkg/types" 20 | ) 21 | 22 | func (i *ImagePullSecret) GetOwnerReferenceForOwnedObject() metav1.OwnerReference { 23 | return metav1.OwnerReference{ 24 | APIVersion: i.APIVersion, 25 | Kind: i.Kind, 26 | Name: i.Name, 27 | UID: i.UID, 28 | Controller: BoolPointer(false), 29 | } 30 | } 31 | 32 | func (r RegistryConfig) CredentialsAsNamespacedNameList() []types.NamespacedName { 33 | list := make([]types.NamespacedName, len(r.Credentials)) 34 | for idx, cred := range r.Credentials { 35 | list[idx] = types.NamespacedName{ 36 | Namespace: cred.Namespace, 37 | Name: cred.Name, 38 | } 39 | } 40 | 41 | return list 42 | } 43 | 44 | func BoolPointer(b bool) *bool { 45 | return &b 46 | } 47 | -------------------------------------------------------------------------------- /api/v1alpha1/imagepullsecret_helpers_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/assert" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/types" 9 | ) 10 | 11 | func TestImagePullSecret_GetOwnerReferenceForOwnedObject(t *testing.T) { 12 | t.Parallel() 13 | testImagePullSecret := ImagePullSecret{ 14 | TypeMeta: metav1.TypeMeta{ 15 | APIVersion: "testAPIVersion", 16 | Kind: "ImagePullSecret", 17 | }, 18 | ObjectMeta: metav1.ObjectMeta{ 19 | Name: "testImagePullSecret", 20 | Namespace: "testNamespace", 21 | UID: "testUID", 22 | }, 23 | } 24 | 25 | tests := []struct { 26 | name string 27 | imagePullSecret ImagePullSecret 28 | want metav1.OwnerReference 29 | }{ 30 | { 31 | name: "basic functionality check", 32 | imagePullSecret: testImagePullSecret, 33 | want: metav1.OwnerReference{ 34 | APIVersion: "testAPIVersion", 35 | Kind: "ImagePullSecret", 36 | Name: "testImagePullSecret", 37 | UID: "testUID", 38 | Controller: BoolPointer(false), 39 | }, 40 | }, 41 | } 42 | for _, tt := range tests { 43 | tt := tt 44 | t.Run(tt.name, func(t *testing.T) { 45 | t.Parallel() 46 | found := tt.imagePullSecret.GetOwnerReferenceForOwnedObject() 47 | 48 | assert.DeepEqual(t, tt.want, found) 49 | }) 50 | } 51 | } 52 | 53 | func TestRegistryConfig_CredentialsAsNamespacedNameList(t *testing.T) { 54 | t.Parallel() 55 | testRegistryConfig := RegistryConfig{ 56 | Credentials: []NamespacedName{ 57 | { 58 | Name: "testCred", 59 | Namespace: "testNamespace", 60 | }, 61 | { 62 | Name: "testCred2", 63 | Namespace: "testNamespace", 64 | }, 65 | { 66 | Name: "testCred3", 67 | Namespace: "testNamespace2", 68 | }, 69 | }, 70 | } 71 | 72 | tests := []struct { 73 | name string 74 | registryConfig RegistryConfig 75 | want []types.NamespacedName 76 | }{ 77 | { 78 | name: "basic functionality check", 79 | registryConfig: testRegistryConfig, 80 | want: []types.NamespacedName{ 81 | { 82 | Name: "testCred", 83 | Namespace: "testNamespace", 84 | }, 85 | { 86 | Name: "testCred2", 87 | Namespace: "testNamespace", 88 | }, 89 | { 90 | Name: "testCred3", 91 | Namespace: "testNamespace2", 92 | }, 93 | }, 94 | }, 95 | } 96 | for _, tt := range tests { 97 | tt := tt 98 | t.Run(tt.name, func(t *testing.T) { 99 | t.Parallel() 100 | found := tt.registryConfig.CredentialsAsNamespacedNameList() 101 | 102 | assert.DeepEqual(t, tt.want, found) 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /api/v1alpha1/imagepullsecret_matchers.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1alpha1 16 | 17 | import ( 18 | "emperror.dev/errors" 19 | corev1 "k8s.io/api/core/v1" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/labels" 22 | ) 23 | 24 | func (s ObjectSelectorConfiguration) Matches(meta metav1.ObjectMeta) (bool, error) { 25 | for _, labelSelector := range s.Labels { 26 | labelSelector := labelSelector 27 | matcher, err := metav1.LabelSelectorAsSelector(&labelSelector) 28 | if err != nil { 29 | return false, err 30 | } 31 | if matcher.Matches(labels.Set(meta.Labels)) { 32 | return true, nil 33 | } 34 | } 35 | 36 | for _, annotationSelector := range s.Annotations { 37 | labelSelectorFromAnnotations := metav1.LabelSelector{ 38 | MatchExpressions: annotationSelector.MatchExpressions, 39 | MatchLabels: annotationSelector.MatchAnnotations, 40 | } 41 | 42 | matcher, err := metav1.LabelSelectorAsSelector(&labelSelectorFromAnnotations) 43 | if err != nil { 44 | return false, err 45 | } 46 | if matcher.Matches(labels.Set(meta.Annotations)) { 47 | return true, nil 48 | } 49 | } 50 | 51 | return false, nil 52 | } 53 | 54 | func (s ObjectSelectorConfiguration) IsEmpty() bool { 55 | return len(s.Annotations) == 0 && len(s.Labels) == 0 56 | } 57 | 58 | func (i ImagePullSecret) MatchesNamespace(ns *corev1.Namespace) (bool, error) { 59 | for _, name := range i.Spec.Target.Namespaces.Names { 60 | if ns.Name == name { 61 | return true, nil 62 | } 63 | } 64 | 65 | match, err := i.Spec.Target.Namespaces.Matches(ns.ObjectMeta) 66 | if err != nil { 67 | return false, err 68 | } 69 | 70 | return match, nil 71 | } 72 | 73 | func (i ImagePullSecret) MatchesPod(pod *corev1.Pod) (bool, error) { 74 | return i.Spec.Target.NamespacesWithPods.Matches(pod.ObjectMeta) 75 | } 76 | 77 | func (i ImagePullSecret) SplitNamespacesByMatch(allNs corev1.NamespaceList) ([]corev1.Namespace, []corev1.Namespace, error) { 78 | match := []corev1.Namespace{} 79 | nonMatch := []corev1.Namespace{} 80 | for _, ns := range allNs.Items { 81 | itemMatches, err := i.MatchesNamespace(ns.DeepCopy()) 82 | if err != nil { 83 | return nil, nil, errors.WrapWithDetails(err, "cannot filter namespaces", map[string]interface{}{ 84 | "ns": ns, 85 | "imps": i, 86 | }) 87 | } 88 | if itemMatches { 89 | match = append(match, ns) 90 | } else { 91 | nonMatch = append(nonMatch, ns) 92 | } 93 | } 94 | return match, nonMatch, nil 95 | } 96 | -------------------------------------------------------------------------------- /api/v1alpha1/imagepullsecret_matchers_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/assert" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestImagePullSecret_SplitNamespacesByMatch(t *testing.T) { 12 | t.Parallel() 13 | type args struct { 14 | allNs corev1.NamespaceList 15 | } 16 | 17 | testImagePullSecret := ImagePullSecret{ 18 | Spec: ImagePullSecretSpec{ 19 | Target: TargetConfig{ 20 | Namespaces: NamespaceSelectorConfiguration{ 21 | Names: []string{"testNamespace", "testNamespace2"}, 22 | }, 23 | }, 24 | }, 25 | } 26 | 27 | tests := []struct { 28 | name string 29 | imagePullSecret ImagePullSecret 30 | args args 31 | expectedMatchingNs []corev1.Namespace 32 | expectedNonMatichingNs []corev1.Namespace 33 | }{ 34 | { 35 | name: "basic functionality check", 36 | imagePullSecret: testImagePullSecret, 37 | args: args{ 38 | allNs: corev1.NamespaceList{ 39 | Items: []corev1.Namespace{ 40 | { 41 | ObjectMeta: metav1.ObjectMeta{ 42 | Name: "testNamespace", 43 | }, 44 | }, 45 | { 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: "testNamespace2", 48 | }, 49 | }, 50 | { 51 | ObjectMeta: metav1.ObjectMeta{ 52 | Name: "testNamespace3", 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | expectedMatchingNs: []corev1.Namespace{ 59 | { 60 | ObjectMeta: metav1.ObjectMeta{ 61 | Name: "testNamespace", 62 | }, 63 | }, 64 | { 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Name: "testNamespace2", 67 | }, 68 | }, 69 | }, 70 | expectedNonMatichingNs: []corev1.Namespace{ 71 | { 72 | ObjectMeta: metav1.ObjectMeta{ 73 | Name: "testNamespace3", 74 | }, 75 | }, 76 | }, 77 | }, 78 | { 79 | name: "no matching namespaces", 80 | imagePullSecret: testImagePullSecret, 81 | args: args{ 82 | allNs: corev1.NamespaceList{ 83 | Items: []corev1.Namespace{ 84 | { 85 | ObjectMeta: metav1.ObjectMeta{ 86 | Name: "testNamespace3", 87 | }, 88 | }, 89 | { 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: "testNamespace4", 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | expectedMatchingNs: []corev1.Namespace{}, 98 | expectedNonMatichingNs: []corev1.Namespace{ 99 | { 100 | ObjectMeta: metav1.ObjectMeta{ 101 | Name: "testNamespace3", 102 | }, 103 | }, 104 | { 105 | ObjectMeta: metav1.ObjectMeta{ 106 | Name: "testNamespace4", 107 | }, 108 | }, 109 | }, 110 | }, 111 | { 112 | name: "all namespaces match", 113 | imagePullSecret: testImagePullSecret, 114 | args: args{ 115 | allNs: corev1.NamespaceList{ 116 | Items: []corev1.Namespace{ 117 | { 118 | ObjectMeta: metav1.ObjectMeta{ 119 | Name: "testNamespace", 120 | }, 121 | }, 122 | { 123 | ObjectMeta: metav1.ObjectMeta{ 124 | Name: "testNamespace2", 125 | }, 126 | }, 127 | }, 128 | }, 129 | }, 130 | expectedMatchingNs: []corev1.Namespace{ 131 | { 132 | ObjectMeta: metav1.ObjectMeta{ 133 | Name: "testNamespace", 134 | }, 135 | }, 136 | { 137 | ObjectMeta: metav1.ObjectMeta{ 138 | Name: "testNamespace2", 139 | }, 140 | }, 141 | }, 142 | expectedNonMatichingNs: []corev1.Namespace{}, 143 | }, 144 | } 145 | for _, tt := range tests { 146 | tt := tt 147 | t.Run(tt.name, func(t *testing.T) { 148 | t.Parallel() 149 | matchingNs, nonMatichingNs, err := tt.imagePullSecret.SplitNamespacesByMatch(tt.args.allNs) 150 | 151 | assert.DeepEqual(t, tt.expectedMatchingNs, matchingNs) 152 | assert.DeepEqual(t, tt.expectedNonMatichingNs, nonMatichingNs) 153 | assert.NilError(t, err) 154 | }) 155 | } 156 | } 157 | 158 | func TestImagePullSecret_MatchesPod(t *testing.T) { 159 | t.Parallel() 160 | type args struct { 161 | pod corev1.Pod 162 | } 163 | 164 | testImagePullSecret := ImagePullSecret{ 165 | Spec: ImagePullSecretSpec{ 166 | Target: TargetConfig{ 167 | NamespacesWithPods: ObjectSelectorConfiguration{ 168 | Labels: []metav1.LabelSelector{ 169 | { 170 | MatchLabels: map[string]string{ 171 | "testLabel": "true", 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | }, 178 | } 179 | 180 | tests := []struct { 181 | name string 182 | imagePullSecret ImagePullSecret 183 | args args 184 | want bool 185 | }{ 186 | { 187 | name: "basic functionality check", 188 | imagePullSecret: testImagePullSecret, 189 | args: args{ 190 | pod: corev1.Pod{ 191 | ObjectMeta: metav1.ObjectMeta{ 192 | Labels: map[string]string{ 193 | "testLabel": "true", 194 | "testLabel2": "true", 195 | }, 196 | }, 197 | }, 198 | }, 199 | want: true, 200 | }, 201 | { 202 | name: "label value different", 203 | imagePullSecret: testImagePullSecret, 204 | args: args{ 205 | pod: corev1.Pod{ 206 | ObjectMeta: metav1.ObjectMeta{ 207 | Labels: map[string]string{ 208 | "testLabel": "false", 209 | "testLabel2": "true", 210 | }, 211 | }, 212 | }, 213 | }, 214 | want: false, 215 | }, 216 | { 217 | name: "different label only", 218 | imagePullSecret: testImagePullSecret, 219 | args: args{ 220 | pod: corev1.Pod{ 221 | ObjectMeta: metav1.ObjectMeta{ 222 | Labels: map[string]string{ 223 | "testLabel2": "true", 224 | }, 225 | }, 226 | }, 227 | }, 228 | want: false, 229 | }, 230 | } 231 | for _, tt := range tests { 232 | tt := tt 233 | t.Run(tt.name, func(t *testing.T) { 234 | t.Parallel() 235 | found, err := tt.imagePullSecret.MatchesPod(&tt.args.pod) 236 | 237 | assert.Equal(t, tt.want, found) 238 | assert.NilError(t, err) 239 | }) 240 | } 241 | } 242 | 243 | func TestImagePullSecret_MatchesNamespace(t *testing.T) { 244 | t.Parallel() 245 | type args struct { 246 | ns corev1.Namespace 247 | } 248 | 249 | testImagePullSecret := ImagePullSecret{ 250 | Spec: ImagePullSecretSpec{ 251 | Target: TargetConfig{ 252 | Namespaces: NamespaceSelectorConfiguration{ 253 | Names: []string{"testNamespace"}, 254 | }, 255 | }, 256 | }, 257 | } 258 | 259 | tests := []struct { 260 | name string 261 | imagePullSecret ImagePullSecret 262 | args args 263 | want bool 264 | }{ 265 | { 266 | name: "namespace matches", 267 | imagePullSecret: testImagePullSecret, 268 | args: args{ 269 | ns: corev1.Namespace{ 270 | ObjectMeta: metav1.ObjectMeta{ 271 | Name: "testNamespace", 272 | }, 273 | }, 274 | }, 275 | want: true, 276 | }, 277 | { 278 | name: "namespace doesn't match", 279 | imagePullSecret: testImagePullSecret, 280 | args: args{ 281 | ns: corev1.Namespace{ 282 | ObjectMeta: metav1.ObjectMeta{ 283 | Name: "testNamespace2", 284 | }, 285 | }, 286 | }, 287 | want: false, 288 | }, 289 | } 290 | for _, tt := range tests { 291 | tt := tt 292 | t.Run(tt.name, func(t *testing.T) { 293 | t.Parallel() 294 | found, err := tt.imagePullSecret.MatchesNamespace(&tt.args.ns) 295 | 296 | assert.Equal(t, tt.want, found) 297 | assert.NilError(t, err) 298 | }) 299 | } 300 | } 301 | 302 | func TestObjectSelectorConfiguration_IsEmpty(t *testing.T) { 303 | t.Parallel() 304 | 305 | testObjectSelectorConfiguration := ObjectSelectorConfiguration{ 306 | Labels: []metav1.LabelSelector{ 307 | { 308 | MatchLabels: map[string]string{ 309 | "testLabel": "true", 310 | }, 311 | }, 312 | }, 313 | Annotations: []AnnotationSelector{ 314 | { 315 | MatchAnnotations: map[string]string{ 316 | "testAnnotation": "true", 317 | }, 318 | }, 319 | }, 320 | } 321 | 322 | tests := []struct { 323 | name string 324 | objectSelectorConfiguration ObjectSelectorConfiguration 325 | want bool 326 | }{ 327 | { 328 | name: "object selector config isn't empty", 329 | objectSelectorConfiguration: testObjectSelectorConfiguration, 330 | want: false, 331 | }, 332 | { 333 | name: "object selector config is empty", 334 | objectSelectorConfiguration: ObjectSelectorConfiguration{}, 335 | want: true, 336 | }, 337 | } 338 | for _, tt := range tests { 339 | tt := tt 340 | t.Run(tt.name, func(t *testing.T) { 341 | t.Parallel() 342 | found := tt.objectSelectorConfiguration.IsEmpty() 343 | 344 | assert.Equal(t, tt.want, found) 345 | }) 346 | } 347 | } 348 | 349 | func TestObjectSelectorConfiguration_Matches(t *testing.T) { 350 | t.Parallel() 351 | type args struct { 352 | meta metav1.ObjectMeta 353 | } 354 | 355 | testObjectSelectorConfiguration := ObjectSelectorConfiguration{ 356 | Labels: []metav1.LabelSelector{ 357 | { 358 | MatchLabels: map[string]string{ 359 | "testLabel": "true", 360 | }, 361 | }, 362 | }, 363 | Annotations: []AnnotationSelector{ 364 | { 365 | MatchAnnotations: map[string]string{ 366 | "testAnnotation": "true", 367 | }, 368 | }, 369 | }, 370 | } 371 | 372 | tests := []struct { 373 | name string 374 | objectSelectorConfiguration ObjectSelectorConfiguration 375 | args args 376 | want bool 377 | }{ 378 | { 379 | name: "label match", 380 | objectSelectorConfiguration: testObjectSelectorConfiguration, 381 | args: args{ 382 | meta: metav1.ObjectMeta{ 383 | Labels: map[string]string{ 384 | "testLabel": "true", 385 | "testLabel2": "true", 386 | }, 387 | }, 388 | }, 389 | want: true, 390 | }, 391 | { 392 | name: "annotation match", 393 | objectSelectorConfiguration: testObjectSelectorConfiguration, 394 | args: args{ 395 | meta: metav1.ObjectMeta{ 396 | Annotations: map[string]string{ 397 | "testAnnotation": "true", 398 | }, 399 | }, 400 | }, 401 | want: true, 402 | }, 403 | { 404 | name: "no matches", 405 | objectSelectorConfiguration: testObjectSelectorConfiguration, 406 | args: args{ 407 | meta: metav1.ObjectMeta{}, 408 | }, 409 | want: false, 410 | }, 411 | } 412 | for _, tt := range tests { 413 | tt := tt 414 | t.Run(tt.name, func(t *testing.T) { 415 | t.Parallel() 416 | found, err := tt.objectSelectorConfiguration.Matches(tt.args.meta) 417 | 418 | assert.Equal(t, tt.want, found) 419 | assert.NilError(t, err) 420 | }) 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /api/v1alpha1/imagepullsecret_types.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // nolint: maligned 16 | package v1alpha1 17 | 18 | import ( 19 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 | ) 21 | 22 | // ImagePullSecretSpec defines the desired state of ImagePullSecret 23 | type ImagePullSecretSpec struct { 24 | // Target specifies what should be the name of the secret created in a 25 | // given namespace 26 | Target TargetConfig `json:"target"` 27 | 28 | // Registry contains the details of the secret to be created in each namespace 29 | Registry RegistryConfig `json:"registry"` 30 | } 31 | 32 | type NamespaceSelectorConfiguration struct { 33 | ObjectSelectorConfiguration `json:",inline"` 34 | // Namespaces specifies additional namespaces by name to generate the secret into 35 | Names []string `json:"names,omitempty"` 36 | } 37 | 38 | type ObjectSelectorConfiguration struct { 39 | // Labels specify the conditions, which are matched against the namespaces labels 40 | // to decide if this ImagePullSecret should be applied to the given namespace, if multiple 41 | // selectors are specified if one is matches the secret will be managed (OR) 42 | Labels []metav1.LabelSelector `json:"labels,omitempty"` 43 | // Selectors specify the conditions, which are matched against the namespaces labels 44 | // to decide if this ImagePullSecret should be applied to the given namespace, if multiple 45 | // selectors are specified if one is matches the secret will be managed (OR) 46 | Annotations []AnnotationSelector `json:"annotations,omitempty"` 47 | } 48 | 49 | type AnnotationSelector struct { 50 | MatchAnnotations map[string]string `json:"matchAnnotations,omitempty"` 51 | MatchExpressions []metav1.LabelSelectorRequirement `json:"matchExpressions,omitempty"` 52 | } 53 | 54 | // TargetConfig describes the secret to be created and the selectors required to determine which namespaces should 55 | // contain this secret 56 | type TargetConfig struct { 57 | Secret TargetSecretConfig `json:"secret"` 58 | // Namespaces specify conditions on the namespaces that should have the TargetSecret generated 59 | Namespaces NamespaceSelectorConfiguration `json:"namespaces,omitempty"` 60 | // Pods specify the conditions, which are matched against the pods in each namespace 61 | // to decide if this ImagePullSecret should be applied to the given pod's namespace, if multiple 62 | // selectors are specified if one is matches the secret will be managed (OR) 63 | NamespacesWithPods ObjectSelectorConfiguration `json:"namespacesWithPods,omitempty"` 64 | } 65 | 66 | // TargetSecretConfig describes the properties of the secrets created in each selected namespace 67 | type TargetSecretConfig struct { 68 | // Name specifies the name of the secret object inside all the selected namespace 69 | Name string `json:"name"` 70 | // Labels specifies additional labels to be put on the Secret object 71 | Labels map[string]string `json:"labels,omitempty"` 72 | // Annotations specifies additional annotations to be put on the Secret object 73 | Annotations map[string]string `json:"annotations,omitempty"` 74 | } 75 | 76 | // RegistryConfig specifies what secret to be used as the basis of the pull secets 77 | type RegistryConfig struct { 78 | // Credentials specifies which secret to be used as the source for docker login credentials 79 | Credentials []NamespacedName `json:"credentials"` 80 | } 81 | 82 | type ReconciliationStatus string 83 | 84 | const ( 85 | ReconciliationReady = "Ready" 86 | ReconciliationFailed = "Failed" 87 | ) 88 | 89 | // ImagePullSecretStatus defines the observed state of ImagePullSecret 90 | type ImagePullSecretStatus struct { 91 | Status ReconciliationStatus `json:"status,omitempty"` 92 | Reason string `json:"reason,omitempty"` 93 | LastSuccessfulReconciliation *metav1.Time `json:"lastSuccessfulReconciliation,omitempty"` 94 | ValiditySeconds int32 `json:"validitySeconds,omitempty"` 95 | ManagedNamespaces []string `json:"managedNamespaces,omitempty"` 96 | SourceSecretStatus map[string]string `json:"sourceSecretStatus,omitempty"` 97 | } 98 | 99 | type NamespacedName struct { 100 | Name string `json:"name"` 101 | Namespace string `json:"namespace"` 102 | } 103 | 104 | // +genclient 105 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 106 | // +genclient:nonNamespaced 107 | 108 | // ImagePullSecret is the Schema for the imagepullsecrets API 109 | // +k8s:openapi-gen=true 110 | // +kubebuilder:subresource:status 111 | // +kubebuilder:resource:path=imagepullsecrets,shortName=imps,scope=Cluster 112 | // +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.status",description="Represents if the object has been successfully reconciled",priority=0,format="byte" 113 | // +kubebuilder:printcolumn:name="Reconciled",type="date",JSONPath=".status.lastSuccessfulReconciliation",description="When the object has been successfully reconciled",priority=0,format="date" 114 | // +kubebuilder:printcolumn:name="Validity seconds",type="integer",JSONPath=".status.validitySeconds",description="How long the generated credential is valid for after the last reconciliation",priority=0,format="int32" 115 | // +kubebuilder:printcolumn:name="Secret Name",type="string",JSONPath=".spec.target.secret.name",description="Name of the secret generated",priority=0,format="byte" 116 | // +kubebuilder:printcolumn:name="Namespaces",type="string",JSONPath=".status.managedNamespaces",description="Name of the namespaces the secret is generated in",priority=0,format="byte" 117 | type ImagePullSecret struct { 118 | metav1.TypeMeta `json:",inline"` 119 | metav1.ObjectMeta `json:"metadata,omitempty"` 120 | 121 | Spec ImagePullSecretSpec `json:"spec,omitempty"` 122 | Status ImagePullSecretStatus `json:"status,omitempty"` 123 | } 124 | 125 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 126 | // +genclient:nonNamespaced 127 | // ImagePullSecretList contains a list of ImagePullSecret 128 | type ImagePullSecretList struct { 129 | metav1.TypeMeta `json:",inline"` 130 | metav1.ListMeta `json:"metadata,omitempty"` 131 | Items []ImagePullSecret `json:"items"` 132 | } 133 | 134 | func init() { 135 | SchemeBuilder.Register(&ImagePullSecret{}, &ImagePullSecretList{}) 136 | } 137 | -------------------------------------------------------------------------------- /api/v1alpha1/set.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package v1alpha1 16 | 17 | import ( 18 | "k8s.io/apimachinery/pkg/labels" 19 | ) 20 | 21 | type LabelSet labels.Set 22 | 23 | func (s LabelSet) DeepCopy() LabelSet { 24 | newSet := LabelSet{} 25 | for k, v := range s { 26 | newSet[k] = v 27 | } 28 | return newSet 29 | } 30 | -------------------------------------------------------------------------------- /api/v1alpha1/set_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/assert" 7 | ) 8 | 9 | func TestLabelSet_DeepCopy(t *testing.T) { 10 | t.Parallel() 11 | testlabelSet := map[string]string{ 12 | "testLabel": "true", 13 | } 14 | 15 | tests := []struct { 16 | name string 17 | labelSet LabelSet 18 | }{ 19 | { 20 | name: "deep copy test", 21 | labelSet: testlabelSet, 22 | }, 23 | } 24 | for _, tt := range tests { 25 | tt := tt 26 | t.Run(tt.name, func(t *testing.T) { 27 | t.Parallel() 28 | labelSetCopy := tt.labelSet.DeepCopy() 29 | 30 | assert.DeepEqual(t, tt.labelSet, labelSetCopy) 31 | assert.Assert(t, &tt.labelSet != &labelSetCopy) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /api/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | // Code generated by controller-gen. DO NOT EDIT. 5 | 6 | package v1alpha1 7 | 8 | import ( 9 | "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | runtime "k8s.io/apimachinery/pkg/runtime" 11 | ) 12 | 13 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 14 | func (in *AnnotationSelector) DeepCopyInto(out *AnnotationSelector) { 15 | *out = *in 16 | if in.MatchAnnotations != nil { 17 | in, out := &in.MatchAnnotations, &out.MatchAnnotations 18 | *out = make(map[string]string, len(*in)) 19 | for key, val := range *in { 20 | (*out)[key] = val 21 | } 22 | } 23 | if in.MatchExpressions != nil { 24 | in, out := &in.MatchExpressions, &out.MatchExpressions 25 | *out = make([]v1.LabelSelectorRequirement, len(*in)) 26 | for i := range *in { 27 | (*in)[i].DeepCopyInto(&(*out)[i]) 28 | } 29 | } 30 | } 31 | 32 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnnotationSelector. 33 | func (in *AnnotationSelector) DeepCopy() *AnnotationSelector { 34 | if in == nil { 35 | return nil 36 | } 37 | out := new(AnnotationSelector) 38 | in.DeepCopyInto(out) 39 | return out 40 | } 41 | 42 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 43 | func (in *ImagePullSecret) DeepCopyInto(out *ImagePullSecret) { 44 | *out = *in 45 | out.TypeMeta = in.TypeMeta 46 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 47 | in.Spec.DeepCopyInto(&out.Spec) 48 | in.Status.DeepCopyInto(&out.Status) 49 | } 50 | 51 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullSecret. 52 | func (in *ImagePullSecret) DeepCopy() *ImagePullSecret { 53 | if in == nil { 54 | return nil 55 | } 56 | out := new(ImagePullSecret) 57 | in.DeepCopyInto(out) 58 | return out 59 | } 60 | 61 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 62 | func (in *ImagePullSecret) DeepCopyObject() runtime.Object { 63 | if c := in.DeepCopy(); c != nil { 64 | return c 65 | } 66 | return nil 67 | } 68 | 69 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 70 | func (in *ImagePullSecretList) DeepCopyInto(out *ImagePullSecretList) { 71 | *out = *in 72 | out.TypeMeta = in.TypeMeta 73 | in.ListMeta.DeepCopyInto(&out.ListMeta) 74 | if in.Items != nil { 75 | in, out := &in.Items, &out.Items 76 | *out = make([]ImagePullSecret, len(*in)) 77 | for i := range *in { 78 | (*in)[i].DeepCopyInto(&(*out)[i]) 79 | } 80 | } 81 | } 82 | 83 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullSecretList. 84 | func (in *ImagePullSecretList) DeepCopy() *ImagePullSecretList { 85 | if in == nil { 86 | return nil 87 | } 88 | out := new(ImagePullSecretList) 89 | in.DeepCopyInto(out) 90 | return out 91 | } 92 | 93 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 94 | func (in *ImagePullSecretList) DeepCopyObject() runtime.Object { 95 | if c := in.DeepCopy(); c != nil { 96 | return c 97 | } 98 | return nil 99 | } 100 | 101 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 102 | func (in *ImagePullSecretSpec) DeepCopyInto(out *ImagePullSecretSpec) { 103 | *out = *in 104 | in.Target.DeepCopyInto(&out.Target) 105 | in.Registry.DeepCopyInto(&out.Registry) 106 | } 107 | 108 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullSecretSpec. 109 | func (in *ImagePullSecretSpec) DeepCopy() *ImagePullSecretSpec { 110 | if in == nil { 111 | return nil 112 | } 113 | out := new(ImagePullSecretSpec) 114 | in.DeepCopyInto(out) 115 | return out 116 | } 117 | 118 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 119 | func (in *ImagePullSecretStatus) DeepCopyInto(out *ImagePullSecretStatus) { 120 | *out = *in 121 | if in.LastSuccessfulReconciliation != nil { 122 | in, out := &in.LastSuccessfulReconciliation, &out.LastSuccessfulReconciliation 123 | *out = (*in).DeepCopy() 124 | } 125 | if in.ManagedNamespaces != nil { 126 | in, out := &in.ManagedNamespaces, &out.ManagedNamespaces 127 | *out = make([]string, len(*in)) 128 | copy(*out, *in) 129 | } 130 | if in.SourceSecretStatus != nil { 131 | in, out := &in.SourceSecretStatus, &out.SourceSecretStatus 132 | *out = make(map[string]string, len(*in)) 133 | for key, val := range *in { 134 | (*out)[key] = val 135 | } 136 | } 137 | } 138 | 139 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImagePullSecretStatus. 140 | func (in *ImagePullSecretStatus) DeepCopy() *ImagePullSecretStatus { 141 | if in == nil { 142 | return nil 143 | } 144 | out := new(ImagePullSecretStatus) 145 | in.DeepCopyInto(out) 146 | return out 147 | } 148 | 149 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 150 | func (in LabelSet) DeepCopyInto(out *LabelSet) { 151 | { 152 | in := &in 153 | *out = in.DeepCopy() 154 | } 155 | } 156 | 157 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 158 | func (in *NamespaceSelectorConfiguration) DeepCopyInto(out *NamespaceSelectorConfiguration) { 159 | *out = *in 160 | in.ObjectSelectorConfiguration.DeepCopyInto(&out.ObjectSelectorConfiguration) 161 | if in.Names != nil { 162 | in, out := &in.Names, &out.Names 163 | *out = make([]string, len(*in)) 164 | copy(*out, *in) 165 | } 166 | } 167 | 168 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceSelectorConfiguration. 169 | func (in *NamespaceSelectorConfiguration) DeepCopy() *NamespaceSelectorConfiguration { 170 | if in == nil { 171 | return nil 172 | } 173 | out := new(NamespaceSelectorConfiguration) 174 | in.DeepCopyInto(out) 175 | return out 176 | } 177 | 178 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 179 | func (in *NamespacedName) DeepCopyInto(out *NamespacedName) { 180 | *out = *in 181 | } 182 | 183 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedName. 184 | func (in *NamespacedName) DeepCopy() *NamespacedName { 185 | if in == nil { 186 | return nil 187 | } 188 | out := new(NamespacedName) 189 | in.DeepCopyInto(out) 190 | return out 191 | } 192 | 193 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 194 | func (in *ObjectSelectorConfiguration) DeepCopyInto(out *ObjectSelectorConfiguration) { 195 | *out = *in 196 | if in.Labels != nil { 197 | in, out := &in.Labels, &out.Labels 198 | *out = make([]v1.LabelSelector, len(*in)) 199 | for i := range *in { 200 | (*in)[i].DeepCopyInto(&(*out)[i]) 201 | } 202 | } 203 | if in.Annotations != nil { 204 | in, out := &in.Annotations, &out.Annotations 205 | *out = make([]AnnotationSelector, len(*in)) 206 | for i := range *in { 207 | (*in)[i].DeepCopyInto(&(*out)[i]) 208 | } 209 | } 210 | } 211 | 212 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectSelectorConfiguration. 213 | func (in *ObjectSelectorConfiguration) DeepCopy() *ObjectSelectorConfiguration { 214 | if in == nil { 215 | return nil 216 | } 217 | out := new(ObjectSelectorConfiguration) 218 | in.DeepCopyInto(out) 219 | return out 220 | } 221 | 222 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 223 | func (in *RegistryConfig) DeepCopyInto(out *RegistryConfig) { 224 | *out = *in 225 | if in.Credentials != nil { 226 | in, out := &in.Credentials, &out.Credentials 227 | *out = make([]NamespacedName, len(*in)) 228 | copy(*out, *in) 229 | } 230 | } 231 | 232 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryConfig. 233 | func (in *RegistryConfig) DeepCopy() *RegistryConfig { 234 | if in == nil { 235 | return nil 236 | } 237 | out := new(RegistryConfig) 238 | in.DeepCopyInto(out) 239 | return out 240 | } 241 | 242 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 243 | func (in *TargetConfig) DeepCopyInto(out *TargetConfig) { 244 | *out = *in 245 | in.Secret.DeepCopyInto(&out.Secret) 246 | in.Namespaces.DeepCopyInto(&out.Namespaces) 247 | in.NamespacesWithPods.DeepCopyInto(&out.NamespacesWithPods) 248 | } 249 | 250 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetConfig. 251 | func (in *TargetConfig) DeepCopy() *TargetConfig { 252 | if in == nil { 253 | return nil 254 | } 255 | out := new(TargetConfig) 256 | in.DeepCopyInto(out) 257 | return out 258 | } 259 | 260 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 261 | func (in *TargetSecretConfig) DeepCopyInto(out *TargetSecretConfig) { 262 | *out = *in 263 | if in.Labels != nil { 264 | in, out := &in.Labels, &out.Labels 265 | *out = make(map[string]string, len(*in)) 266 | for key, val := range *in { 267 | (*out)[key] = val 268 | } 269 | } 270 | if in.Annotations != nil { 271 | in, out := &in.Annotations, &out.Annotations 272 | *out = make(map[string]string, len(*in)) 273 | for key, val := range *in { 274 | (*out)[key] = val 275 | } 276 | } 277 | } 278 | 279 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSecretConfig. 280 | func (in *TargetSecretConfig) DeepCopy() *TargetSecretConfig { 281 | if in == nil { 282 | return nil 283 | } 284 | out := new(TargetSecretConfig) 285 | in.DeepCopyInto(out) 286 | return out 287 | } 288 | -------------------------------------------------------------------------------- /cmd/controller/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021 Banzai Cloud Zrt. All Rights Reserved. 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "strings" 9 | "time" 10 | 11 | // +kubebuilder:scaffold:imports 12 | "github.com/cisco-open/operator-tools/pkg/reconciler" 13 | "github.com/spf13/pflag" 14 | "github.com/spf13/viper" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 17 | logrintegration "logur.dev/integration/logr" 18 | "logur.dev/logur" 19 | ctrl "sigs.k8s.io/controller-runtime" 20 | 21 | "github.com/banzaicloud/imps/api/v1alpha1" 22 | "github.com/banzaicloud/imps/controllers" 23 | "github.com/banzaicloud/imps/internal/errorhandler" 24 | "github.com/banzaicloud/imps/internal/log" 25 | "github.com/banzaicloud/imps/pkg/ecr" 26 | ) 27 | 28 | var ( 29 | scheme = runtime.NewScheme() 30 | setupLog = ctrl.Log.WithName("setup") 31 | ) 32 | 33 | func init() { 34 | _ = clientgoscheme.AddToScheme(scheme) 35 | 36 | _ = v1alpha1.AddToScheme(scheme) 37 | 38 | // +kubebuilder:scaffold:scheme 39 | } 40 | 41 | type Config struct { 42 | Log log.Config 43 | } 44 | 45 | func main() { 46 | Configure(viper.GetViper(), pflag.CommandLine) 47 | 48 | var metricsAddr string 49 | var periodicReconcileInterval int 50 | var enableLeaderElection bool 51 | var configNamespace string 52 | 53 | pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 54 | pflag.IntVar(&periodicReconcileInterval, "periodic-reconcile-interval", 300, "The interval in seconds in which controller reconciles are run periodically.") 55 | pflag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 56 | "Enable leader election for controller manager. "+ 57 | "Enabling this will ensure there is only one active controller manager.") 58 | pflag.StringVar(&configNamespace, "config-namespace", "backyards-system", "The namespace in which internal resources should be created for leader election.") 59 | 60 | var config Config 61 | err := viper.Unmarshal(&config) 62 | if err != nil { 63 | setupLog.Error(err, "failed to unmarshal configuration") 64 | os.Exit(1) 65 | } 66 | 67 | pflag.Parse() 68 | 69 | periodicReconcileIntervalDuration := time.Duration(periodicReconcileInterval) * time.Second 70 | 71 | // Create logger (first thing after configuration loading) 72 | logger := log.NewLogger(config.Log) 73 | ctrl.SetLogger(logrintegration.New(logger)) 74 | 75 | errorHandler := errorhandler.New(logger) 76 | 77 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 78 | Scheme: scheme, 79 | MetricsBindAddress: metricsAddr, 80 | Port: 9443, 81 | LeaderElection: enableLeaderElection, 82 | LeaderElectionID: "73de1ad9.banzaicloud.io", 83 | LeaderElectionNamespace: configNamespace, 84 | }) 85 | if err != nil { 86 | setupLog.Error(err, "unable to start manager") 87 | os.Exit(1) 88 | } 89 | 90 | ecrLogger := logur.WithField(logger, "controller", "ecr_token_refresh") 91 | ecr.Initialize(ecrLogger) 92 | 93 | impsLogger := logur.WithField(logger, "controller", "imagepullsecrets") 94 | impsReconciler := &controllers.ImagePullSecretReconciler{ 95 | Client: mgr.GetClient(), 96 | Log: impsLogger, 97 | ErrorHandler: errorHandler, 98 | ResourceReconciler: reconciler.NewReconcilerWith(mgr.GetClient(), reconciler.WithLog(logrintegration.New(impsLogger))), 99 | Recorder: mgr.GetEventRecorderFor("imagepullsecrets-controller"), 100 | PeriodicReconcileInterval: periodicReconcileIntervalDuration, 101 | } 102 | 103 | if err = impsReconciler.SetupWithManager(mgr); err != nil { 104 | setupLog.Error(err, "unable to create controller", "controller", "imagepullsecrets") 105 | os.Exit(1) 106 | } 107 | 108 | // +kubebuilder:scaffold:builder 109 | 110 | setupLog.Info("starting manager") 111 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 112 | setupLog.Error(err, "problem running manager") 113 | os.Exit(1) 114 | } 115 | } 116 | 117 | const FriendlyServiceName = "imps" 118 | 119 | func Configure(v *viper.Viper, p *pflag.FlagSet) { 120 | v.AllowEmptyEnv(true) 121 | p.Init(FriendlyServiceName, pflag.ExitOnError) 122 | pflag.Usage = func() { 123 | _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", FriendlyServiceName) 124 | pflag.PrintDefaults() 125 | } 126 | 127 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 128 | v.AutomaticEnv() 129 | 130 | log.ConfigureLoggingFlags(v, p) 131 | 132 | _ = v.BindPFlags(p) 133 | } 134 | -------------------------------------------------------------------------------- /cmd/refresher/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "emperror.dev/errors" 10 | "github.com/cisco-open/operator-tools/pkg/reconciler" 11 | "github.com/spf13/pflag" 12 | "github.com/spf13/viper" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/types" 15 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 16 | logrintegration "logur.dev/integration/logr" 17 | "logur.dev/logur" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | 20 | "github.com/banzaicloud/imps/controllers" 21 | "github.com/banzaicloud/imps/internal/errorhandler" 22 | "github.com/banzaicloud/imps/internal/log" 23 | "github.com/banzaicloud/imps/pkg/ecr" 24 | ) 25 | 26 | var ( 27 | scheme = runtime.NewScheme() 28 | setupLog = ctrl.Log.WithName("setup") 29 | ErrInvalidReference = errors.New("invalid resource reference name") 30 | ErrNoSourceSecrets = errors.New("no source secrets are specified") 31 | ) 32 | 33 | func init() { 34 | _ = clientgoscheme.AddToScheme(scheme) 35 | 36 | // +kubebuilder:scaffold:scheme 37 | } 38 | 39 | type Config struct { 40 | Log log.Config 41 | } 42 | 43 | func refToNamespacedName(name string) (*types.NamespacedName, error) { 44 | parts := strings.Split(name, ".") 45 | if len(parts) <= 1 || len(parts) > 2 { 46 | return nil, errors.WrapWithDetails(ErrInvalidReference, "reference", name) 47 | } 48 | 49 | return &types.NamespacedName{ 50 | Namespace: parts[0], 51 | Name: parts[1], 52 | }, nil 53 | } 54 | 55 | func main() { 56 | Configure(viper.GetViper(), pflag.CommandLine) 57 | var periodicReconcileInterval int 58 | var targetSecretString string 59 | var metricsAddr string 60 | 61 | sourceSecretStrings := pflag.StringArray("source-secret", nil, "Source secrets specified in . format") 62 | pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 63 | pflag.IntVar(&periodicReconcileInterval, "periodic-reconcile-interval", 300, "The interval in seconds in which controller reconciles are run periodically.") 64 | pflag.StringVar(&targetSecretString, "target-secret", "", "Target secret specifies what secret to create containing the image pull secrets. Format: namespace.secret-name") 65 | pflag.Parse() 66 | 67 | var config Config 68 | err := viper.Unmarshal(&config) 69 | if err != nil { 70 | setupLog.Error(err, "failed to unmarshal configuration") 71 | os.Exit(1) 72 | } 73 | 74 | // Parse command line arguments 75 | targetSecret, err := refToNamespacedName(targetSecretString) 76 | if err != nil { 77 | setupLog.Error(err, "failed to parse target secret name") 78 | os.Exit(1) 79 | } 80 | 81 | if len(*sourceSecretStrings) == 0 { 82 | setupLog.Error(ErrNoSourceSecrets, "please specify source secrets") 83 | os.Exit(1) 84 | } 85 | 86 | sourceSecrets := []types.NamespacedName{} 87 | for _, sourceSecertString := range *sourceSecretStrings { 88 | sourceSecret, err := refToNamespacedName(sourceSecertString) 89 | if err != nil { 90 | setupLog.Error(err, "failed to parse source secret name") 91 | os.Exit(1) 92 | } 93 | sourceSecrets = append(sourceSecrets, *sourceSecret) 94 | } 95 | 96 | // Create logger (first thing after configuration loading) 97 | logger := log.NewLogger(config.Log) 98 | ctrl.SetLogger(logrintegration.New(logger)) 99 | 100 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 101 | Scheme: scheme, 102 | MetricsBindAddress: metricsAddr, 103 | }) 104 | if err != nil { 105 | setupLog.Error(err, "unable to start manager") 106 | os.Exit(1) 107 | } 108 | 109 | ecrLogger := logur.WithField(logger, "controller", "ecr_token_refresh") 110 | ecr.Initialize(ecrLogger) 111 | 112 | errorHandler := errorhandler.New(logger) 113 | 114 | periodicReconcileIntervalDuration := time.Duration(periodicReconcileInterval) * time.Second 115 | 116 | refresherLogger := logur.WithField(logger, "controller", "imagepullsecrets") 117 | refresherReconciler := &controllers.RefresherReconciler{ 118 | Client: mgr.GetClient(), 119 | Log: refresherLogger, 120 | ErrorHandler: errorHandler, 121 | ResourceReconciler: reconciler.NewReconcilerWith(mgr.GetClient(), reconciler.WithLog(logrintegration.New(refresherLogger))), 122 | PeriodicReconcileInterval: periodicReconcileIntervalDuration, 123 | SourceSecrets: sourceSecrets, 124 | TargetSecret: *targetSecret, 125 | } 126 | 127 | if err = refresherReconciler.SetupWithManager(mgr); err != nil { 128 | setupLog.Error(err, "unable to create controller", "controller", "imagepullsecrets") 129 | os.Exit(1) 130 | } 131 | 132 | setupLog.Info("starting manager") 133 | 134 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 135 | setupLog.Error(err, "problem running manager") 136 | os.Exit(1) 137 | } 138 | } 139 | 140 | const FriendlyServiceName = "imps" 141 | 142 | func Configure(v *viper.Viper, p *pflag.FlagSet) { 143 | v.AllowEmptyEnv(true) 144 | p.Init(FriendlyServiceName, pflag.ExitOnError) 145 | pflag.Usage = func() { 146 | _, _ = fmt.Fprintf(os.Stderr, "Usage of %s:\n", FriendlyServiceName) 147 | pflag.PrintDefaults() 148 | } 149 | 150 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 151 | v.AutomaticEnv() 152 | 153 | log.ConfigureLoggingFlags(v, p) 154 | 155 | _ = v.BindPFlags(p) 156 | } 157 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/images.banzaicloud.io_imagepullsecrets.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | 10 | # the following config is for teaching kustomize how to do kustomization for CRDs. 11 | configurations: 12 | - kustomizeconfig.yaml 13 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: binary-role 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - images.banzaicloud.io 23 | resources: 24 | - imagepullsecrets 25 | verbs: 26 | - create 27 | - delete 28 | - get 29 | - list 30 | - patch 31 | - update 32 | - watch 33 | - apiGroups: 34 | - images.banzaicloud.io 35 | resources: 36 | - imagepullsecrets/status 37 | verbs: 38 | - get 39 | - patch 40 | - update 41 | -------------------------------------------------------------------------------- /config/samples/images_v1alpha1_imagepullsecrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: images.banzaicloud.io/v1alpha1 2 | kind: ImagePullSecrets 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: imagepullsecrets-sample 7 | spec: 8 | # Add fields here 9 | foo: bar 10 | -------------------------------------------------------------------------------- /controllers/const.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package controllers 16 | 17 | const ( 18 | labelImpsOwnerUID = "images.banzaicloud.io/owner-uid" 19 | ) 20 | -------------------------------------------------------------------------------- /controllers/env_setup_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | "testing" 7 | "time" 8 | 9 | "github.com/cisco-open/operator-tools/pkg/reconciler" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | "k8s.io/client-go/kubernetes/scheme" 13 | "k8s.io/client-go/rest" 14 | logrintegration "logur.dev/integration/logr" 15 | "logur.dev/logur" 16 | ctrl "sigs.k8s.io/controller-runtime" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sigs.k8s.io/controller-runtime/pkg/envtest" 19 | logf "sigs.k8s.io/controller-runtime/pkg/log" 20 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 21 | 22 | apiv1 "github.com/banzaicloud/imps/api/v1alpha1" 23 | logging "github.com/banzaicloud/imps/internal/log" 24 | ) 25 | 26 | var ( 27 | cfg *rest.Config 28 | k8sClient client.Client 29 | testEnv *envtest.Environment 30 | ctx context.Context 31 | cancel context.CancelFunc 32 | ) 33 | 34 | func TestControllers(t *testing.T) { 35 | t.Parallel() 36 | RegisterFailHandler(Fail) 37 | 38 | RunSpecs(t, "Controller Suite") 39 | } 40 | 41 | var _ = BeforeSuite(func() { 42 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 43 | 44 | ctx, cancel = context.WithCancel(context.TODO()) 45 | 46 | By("bootstrapping test environment") 47 | testEnv = &envtest.Environment{ 48 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 49 | ErrorIfCRDPathMissing: true, 50 | } 51 | 52 | var err error 53 | cfg, err = testEnv.Start() 54 | Expect(err).NotTo(HaveOccurred()) 55 | Expect(cfg).NotTo(BeNil()) 56 | 57 | err = apiv1.AddToScheme(scheme.Scheme) 58 | Expect(err).NotTo(HaveOccurred()) 59 | 60 | //+kubebuilder:scaffold:scheme 61 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 62 | Expect(err).NotTo(HaveOccurred()) 63 | Expect(k8sClient).NotTo(BeNil()) 64 | 65 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 66 | Scheme: scheme.Scheme, 67 | }) 68 | Expect(err).ToNot(HaveOccurred()) 69 | 70 | logger := logging.NewLogger(logging.Config{}) 71 | ctrl.SetLogger(logrintegration.New(logger)) 72 | 73 | impsLogger := logur.WithField(logger, "controller", "imagepullsecrets") 74 | 75 | impsReconciler := &ImagePullSecretReconciler{ 76 | Client: k8sManager.GetClient(), 77 | ResourceReconciler: reconciler.NewReconcilerWith(k8sManager.GetClient()), 78 | Recorder: k8sManager.GetEventRecorderFor("imagepullsecrets-controller"), 79 | PeriodicReconcileInterval: 300 * time.Second, 80 | Log: impsLogger, 81 | } 82 | err = impsReconciler.SetupWithManager(k8sManager) 83 | Expect(err).ToNot(HaveOccurred()) 84 | 85 | go func() { 86 | defer GinkgoRecover() 87 | err = k8sManager.Start(ctx) 88 | Expect(err).ToNot(HaveOccurred(), "failed to run manager") 89 | }() 90 | }) 91 | 92 | var _ = AfterSuite(func() { 93 | cancel() 94 | By("tearing down the test environment") 95 | err := testEnv.Stop() 96 | Expect(err).NotTo(HaveOccurred()) 97 | }) 98 | -------------------------------------------------------------------------------- /controllers/imagepullsecret_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package controllers 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "emperror.dev/emperror" 22 | "github.com/cisco-open/operator-tools/pkg/reconciler" 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | "k8s.io/client-go/tools/record" 26 | "logur.dev/logur" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | ctrlBuilder "sigs.k8s.io/controller-runtime/pkg/builder" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/handler" 31 | "sigs.k8s.io/controller-runtime/pkg/predicate" 32 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 | "sigs.k8s.io/controller-runtime/pkg/source" 34 | 35 | "github.com/banzaicloud/imps/api/v1alpha1" 36 | "github.com/banzaicloud/imps/internal/cron" 37 | ) 38 | 39 | // AlertingPolicyReconciler reconciles a AlertingPolicy object 40 | type ImagePullSecretReconciler struct { 41 | client.Client 42 | Log logur.Logger 43 | ErrorHandler emperror.ErrorHandler 44 | Recorder record.EventRecorder 45 | 46 | ResourceReconciler reconciler.ResourceReconciler 47 | PeriodicReconcileInterval time.Duration 48 | } 49 | 50 | // +kubebuilder:rbac:groups=images.banzaicloud.io,resources=imagepullsecrets,verbs=get;list;watch;create;update;patch;delete 51 | // +kubebuilder:rbac:groups=images.banzaicloud.io,resources=imagepullsecrets/status,verbs=get;update;patch 52 | // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete 53 | // +kubebuilder:rbac:groups="",resources=events,verbs=create;update;patch 54 | func (r *ImagePullSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 55 | result, err := r.reconcile(ctx, req) 56 | result, err = cron.EnsurePeriodicReconcile(r.PeriodicReconcileInterval, result, err) 57 | if err != nil { 58 | r.ErrorHandler.Handle(err) 59 | } 60 | 61 | return result, err 62 | } 63 | 64 | func (r *ImagePullSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { 65 | builder := ctrl.NewControllerManagedBy(mgr). 66 | For(&v1alpha1.ImagePullSecret{}, ctrlBuilder.WithPredicates(predicate.GenerationChangedPredicate{})). 67 | Watches( 68 | &source.Kind{Type: &corev1.Namespace{}}, 69 | handler.EnqueueRequestsFromMapFunc(func(object client.Object) []reconcile.Request { 70 | return r.impsMatchingNamespace(object) 71 | })). 72 | Watches( 73 | &source.Kind{Type: &corev1.Pod{}}, 74 | handler.EnqueueRequestsFromMapFunc(func(object client.Object) []reconcile.Request { 75 | return r.impsMatchingPod(object) 76 | })). 77 | Watches( 78 | &source.Kind{Type: &corev1.Secret{}}, 79 | handler.EnqueueRequestsFromMapFunc(func(object client.Object) []reconcile.Request { 80 | return r.impsReferencingSecret(object) 81 | })) 82 | 83 | return builder.Complete(r) 84 | } 85 | 86 | func (r *ImagePullSecretReconciler) impsMatchingNamespace(obj client.Object) []ctrl.Request { 87 | ns, ok := obj.(*corev1.Namespace) 88 | if !ok { 89 | r.Log.Info("object is not a Namespace") 90 | 91 | return []ctrl.Request{} 92 | } 93 | 94 | impsList := &v1alpha1.ImagePullSecretList{} 95 | 96 | err := r.Client.List(context.TODO(), impsList) 97 | if err != nil { 98 | r.Log.Info(err.Error()) 99 | 100 | return []ctrl.Request{} 101 | } 102 | 103 | var res []ctrl.Request 104 | for _, imps := range impsList.Items { 105 | matches, err := imps.MatchesNamespace(ns) 106 | if err != nil { 107 | r.Log.Info("cannot match imps against namespace", map[string]interface{}{ 108 | "imps": imps, 109 | "namespace": ns, 110 | "error": err, 111 | }) 112 | 113 | continue 114 | } 115 | 116 | if matches { 117 | res = append(res, reconcile.Request{ 118 | NamespacedName: types.NamespacedName{ 119 | Name: imps.GetName(), 120 | Namespace: imps.GetNamespace(), 121 | }, 122 | }) 123 | } 124 | } 125 | 126 | return res 127 | } 128 | 129 | func (r *ImagePullSecretReconciler) impsMatchingPod(obj client.Object) []ctrl.Request { 130 | pod, ok := obj.(*corev1.Pod) 131 | if !ok { 132 | r.Log.Info("object is not a Pod or Namespace") 133 | 134 | return []ctrl.Request{} 135 | } 136 | 137 | // If the namespace containing the pod matches, let's not add the pod to the reconciliation queue. 138 | // This prevents reconciliations to start on each pod startup when the namespace selectors are properly used. 139 | podsNamespace := &corev1.Namespace{} 140 | err := r.Client.Get(context.TODO(), types.NamespacedName{ 141 | Name: pod.Namespace, 142 | }, podsNamespace) 143 | if err != nil { 144 | r.Log.Warn("cannot get pod's namespace, please authorize the controller to list namespaces, or too many reconcilations will be executed", map[string]interface{}{ 145 | "error": err, 146 | "namespace": pod.Namespace, 147 | }) 148 | podsNamespace = nil 149 | } 150 | 151 | impsList := &v1alpha1.ImagePullSecretList{} 152 | 153 | err = r.Client.List(context.TODO(), impsList) 154 | if err != nil { 155 | r.Log.Info(err.Error()) 156 | 157 | return []ctrl.Request{} 158 | } 159 | var res []ctrl.Request 160 | for _, imps := range impsList.Items { 161 | matches, err := imps.MatchesPod(pod) 162 | if err != nil { 163 | r.Log.Info("cannot match imps against pod", map[string]interface{}{ 164 | "imps": imps, 165 | "pod": pod, 166 | "error": err, 167 | }) 168 | 169 | continue 170 | } 171 | 172 | if matches { 173 | if podsNamespace != nil { 174 | nsMatches, err := imps.MatchesNamespace(podsNamespace) 175 | if err != nil { 176 | r.Log.Info("cannot match imps against namespace", map[string]interface{}{ 177 | "imps": imps, 178 | "namespace": podsNamespace, 179 | "error": err, 180 | }) 181 | } else if nsMatches { 182 | continue 183 | } 184 | } 185 | res = append(res, ctrl.Request{ 186 | NamespacedName: types.NamespacedName{ 187 | Name: imps.GetName(), 188 | Namespace: imps.GetNamespace(), 189 | }, 190 | }) 191 | } 192 | } 193 | 194 | return res 195 | } 196 | 197 | func (r *ImagePullSecretReconciler) impsReferencingSecret(obj client.Object) []ctrl.Request { 198 | secret, ok := obj.(*corev1.Secret) 199 | if !ok { 200 | r.Log.Info("object is not a Secret") 201 | 202 | return []ctrl.Request{} 203 | } 204 | 205 | impsList := &v1alpha1.ImagePullSecretList{} 206 | 207 | err := r.Client.List(context.TODO(), impsList) 208 | if err != nil { 209 | r.Log.Info(err.Error()) 210 | 211 | return []ctrl.Request{} 212 | } 213 | 214 | var res []ctrl.Request 215 | for _, imps := range impsList.Items { 216 | for _, credential := range imps.Spec.Registry.Credentials { 217 | if credential.Name == secret.Name && credential.Namespace == secret.Namespace { 218 | res = append(res, ctrl.Request{ 219 | NamespacedName: types.NamespacedName{ 220 | Name: imps.GetName(), 221 | Namespace: imps.GetNamespace(), 222 | }, 223 | }) 224 | } 225 | } 226 | } 227 | 228 | return res 229 | } 230 | -------------------------------------------------------------------------------- /controllers/imps_integration_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | 13 | apiv1 "github.com/banzaicloud/imps/api/v1alpha1" 14 | ) 15 | 16 | // +kubebuilder:docs-gen:collapse=Imports 17 | 18 | var _ = Describe("IMPS controller", func() { 19 | // Define utility constants for object names and testing timeouts/durations and intervals. 20 | const ( 21 | ImpsName = "test-imps" 22 | ImpsNamespace = "source-ns" 23 | SourceSecretName = "source-secret-1" 24 | TargetSecretName = "target-secret" 25 | TargetSecretNamespace = "target-ns" 26 | 27 | timeout = time.Second * 20 28 | interval = time.Millisecond * 250 29 | ) 30 | 31 | Describe("When creating an IMPS resource", Ordered, func() { 32 | It("Secret should be created in the annotated namespace", func() { 33 | ctx := context.Background() 34 | 35 | sourceNamespace := &v1.Namespace{ 36 | ObjectMeta: metav1.ObjectMeta{ 37 | Name: ImpsNamespace, 38 | Annotations: map[string]string{"test.io/test-annotation": "test-value"}, 39 | }, 40 | } 41 | Expect(k8sClient.Create(ctx, sourceNamespace)).Should(Succeed()) 42 | 43 | targetNamespace := &v1.Namespace{ 44 | ObjectMeta: metav1.ObjectMeta{ 45 | Name: TargetSecretNamespace, 46 | Annotations: map[string]string{"test.io/test-annotation": "test-value"}, 47 | }, 48 | } 49 | Expect(k8sClient.Create(ctx, targetNamespace)).Should(Succeed()) 50 | 51 | sourceSecret := &v1.Secret{ 52 | ObjectMeta: metav1.ObjectMeta{ 53 | Name: SourceSecretName, 54 | Namespace: ImpsNamespace, 55 | }, 56 | StringData: map[string]string{".dockerconfigjson": "{\n \"auths\": {\n \"my-registry.example:5000\": {\n \"username\": \"tiger\",\n \"password\": \"pass1234\",\n \"email\": \"tiger@acme.example\",\n \"auth\": \"dGlnZXI6cGFzczEyMzQ=\"\n }\n }\n}"}, 57 | Type: "kubernetes.io/dockerconfigjson", 58 | } 59 | Expect(k8sClient.Create(ctx, sourceSecret)).Should(Succeed()) 60 | 61 | pullSecret := &apiv1.ImagePullSecret{ 62 | ObjectMeta: metav1.ObjectMeta{ 63 | Name: ImpsName, 64 | Namespace: ImpsNamespace, 65 | }, 66 | Spec: apiv1.ImagePullSecretSpec{ 67 | Target: apiv1.TargetConfig{ 68 | Secret: apiv1.TargetSecretConfig{ 69 | Name: TargetSecretName, 70 | }, 71 | Namespaces: apiv1.NamespaceSelectorConfiguration{ 72 | Names: []string{TargetSecretNamespace}, 73 | }, 74 | }, 75 | Registry: apiv1.RegistryConfig{ 76 | Credentials: []apiv1.NamespacedName{ 77 | { 78 | Name: SourceSecretName, 79 | Namespace: ImpsNamespace, 80 | }, 81 | }, 82 | }, 83 | }, 84 | } 85 | Expect(k8sClient.Create(ctx, pullSecret)).Should(Succeed()) 86 | 87 | secretLookupKey := types.NamespacedName{Name: TargetSecretName, Namespace: TargetSecretNamespace} 88 | createdSecret := &v1.Secret{} 89 | 90 | By("TODO") 91 | Eventually(func() bool { 92 | err := k8sClient.Get(ctx, secretLookupKey, createdSecret) 93 | if err != nil { //nolint:gosimple 94 | return false 95 | } 96 | 97 | return true 98 | }, timeout, interval).Should(BeTrue()) 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /controllers/refresher_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package controllers 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "emperror.dev/emperror" 22 | "emperror.dev/errors" 23 | "github.com/cisco-open/operator-tools/pkg/reconciler" 24 | corev1 "k8s.io/api/core/v1" 25 | "k8s.io/apimachinery/pkg/types" 26 | "logur.dev/logur" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | ctrlBuilder "sigs.k8s.io/controller-runtime/pkg/builder" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/handler" 31 | "sigs.k8s.io/controller-runtime/pkg/predicate" 32 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 33 | "sigs.k8s.io/controller-runtime/pkg/source" 34 | 35 | "github.com/banzaicloud/imps/internal/cron" 36 | "github.com/banzaicloud/imps/pkg/pullsecrets" 37 | ) 38 | 39 | // RefresherReconciler reconciles a AlertingPolicy object 40 | type RefresherReconciler struct { 41 | client.Client 42 | Log logur.Logger 43 | ErrorHandler emperror.ErrorHandler 44 | 45 | ResourceReconciler reconciler.ResourceReconciler 46 | PeriodicReconcileInterval time.Duration 47 | SourceSecrets []types.NamespacedName 48 | TargetSecret types.NamespacedName 49 | } 50 | 51 | // +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete 52 | func (r *RefresherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 53 | result, err := r.reconcile(ctx, req) 54 | result, err = cron.EnsurePeriodicReconcile(r.PeriodicReconcileInterval, result, err) 55 | if err != nil { 56 | r.ErrorHandler.Handle(err) 57 | } 58 | 59 | return result, err 60 | } 61 | 62 | func (r *RefresherReconciler) SetupWithManager(mgr ctrl.Manager) error { 63 | builder := ctrl.NewControllerManagedBy(mgr). 64 | For(&corev1.Secret{}, ctrlBuilder.WithPredicates(predicate.GenerationChangedPredicate{})). 65 | Watches( 66 | &source.Kind{Type: &corev1.Secret{}}, 67 | handler.EnqueueRequestsFromMapFunc(func(object client.Object) []reconcile.Request { 68 | return r.isMatchingSecret(object) 69 | })) 70 | 71 | return builder.Complete(r) 72 | } 73 | 74 | func (r *RefresherReconciler) isMatchingSecret(obj client.Object) []ctrl.Request { 75 | secret, ok := obj.(*corev1.Secret) 76 | if !ok { 77 | r.Log.Info("object is not a Secret") 78 | 79 | return []ctrl.Request{} 80 | } 81 | 82 | reconcileTargetRequest := ctrl.Request{ 83 | NamespacedName: r.TargetSecret, 84 | } 85 | 86 | secretRef := types.NamespacedName{Namespace: secret.Namespace, Name: secret.Name} 87 | 88 | if r.isSourceSecret(secretRef) || r.isTargetSecret(secretRef) { 89 | return []ctrl.Request{reconcileTargetRequest} 90 | } 91 | 92 | return []ctrl.Request{} 93 | } 94 | 95 | func (r *RefresherReconciler) isSourceSecret(secret types.NamespacedName) bool { 96 | for _, sourceSecret := range r.SourceSecrets { 97 | if sourceSecret.Namespace == secret.Namespace && sourceSecret.Name == secret.Name { 98 | return true 99 | } 100 | } 101 | 102 | return false 103 | } 104 | 105 | func (r *RefresherReconciler) isTargetSecret(secret types.NamespacedName) bool { 106 | return r.TargetSecret.Namespace == secret.Namespace && r.TargetSecret.Name == secret.Name 107 | } 108 | 109 | func (r *RefresherReconciler) reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 110 | logger := logur.WithField(r.Log, "imagepullsecret", req.NamespacedName) 111 | result := ctrl.Result{} 112 | 113 | if !r.isSourceSecret(req.NamespacedName) && !r.isTargetSecret(req.NamespacedName) { 114 | return result, nil 115 | } 116 | 117 | config := pullsecrets.NewConfigFromSecrets(ctx, r, r.SourceSecrets) 118 | resultingConfig, err := config.ResultingDockerConfig(ctx) 119 | if err != nil { 120 | return result, errors.WithStack(err) 121 | } 122 | 123 | pullSecret := resultingConfig.AsSecret(r.TargetSecret.Namespace, r.TargetSecret.Name) 124 | 125 | _, err = r.ResourceReconciler.ReconcileResource(pullSecret, reconciler.StatePresent) 126 | if err != nil { 127 | return result, err 128 | } 129 | 130 | if err := resultingConfig.AsError(); err != nil { 131 | for secret, status := range resultingConfig.AsStatus() { 132 | if status != pullsecrets.SourceSecretStatus { 133 | logger.Warn("secret failed to render", map[string]interface{}{ 134 | "source_secret": secret, 135 | "reason": status, 136 | }) 137 | } 138 | } 139 | 140 | return result, err 141 | } 142 | 143 | logger.Info("successfully reconciled secret", map[string]interface{}{ 144 | "expiration": resultingConfig.Expiration, 145 | }) 146 | 147 | return result, nil 148 | } 149 | -------------------------------------------------------------------------------- /controllers/refresher_controller_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/types" 10 | testlogur "logur.dev/logur" 11 | ctrl "sigs.k8s.io/controller-runtime" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | ) 14 | 15 | func TestRefresherReconciler_isTargetSecret(t *testing.T) { 16 | t.Parallel() 17 | type args struct { 18 | secret types.NamespacedName 19 | } 20 | 21 | testRefresherReconciler := RefresherReconciler{} 22 | testRefresherReconciler.TargetSecret = types.NamespacedName{ 23 | Name: "testSecretName", 24 | Namespace: "testSecretNamespace", 25 | } 26 | 27 | tests := []struct { 28 | name string 29 | refresherReconciler RefresherReconciler 30 | args args 31 | want bool 32 | }{ 33 | { 34 | name: "name and namespace both match", 35 | refresherReconciler: testRefresherReconciler, 36 | args: args{ 37 | secret: types.NamespacedName{ 38 | Name: "testSecretName", 39 | Namespace: "testSecretNamespace", 40 | }, 41 | }, 42 | want: true, 43 | }, 44 | { 45 | name: "name and namespace mismatch", 46 | refresherReconciler: testRefresherReconciler, 47 | args: args{ 48 | secret: types.NamespacedName{ 49 | Name: "testSecretName2", 50 | Namespace: "testSecretName2", 51 | }, 52 | }, 53 | want: false, 54 | }, 55 | { 56 | name: "name mismatch", 57 | refresherReconciler: testRefresherReconciler, 58 | args: args{ 59 | secret: types.NamespacedName{ 60 | Name: "testSecretName2", 61 | Namespace: "testSecretName", 62 | }, 63 | }, 64 | want: false, 65 | }, 66 | { 67 | name: "namespace mismatch", 68 | refresherReconciler: testRefresherReconciler, 69 | args: args{ 70 | secret: types.NamespacedName{ 71 | Name: "testSecretName", 72 | Namespace: "testSecretName2", 73 | }, 74 | }, 75 | want: false, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | tt := tt 80 | t.Run(tt.name, func(t *testing.T) { 81 | t.Parallel() 82 | found := tt.refresherReconciler.isTargetSecret(tt.args.secret) 83 | 84 | assert.Equal(t, tt.want, found) 85 | }) 86 | } 87 | } 88 | 89 | func TestRefresherReconciler_isSourceSecret(t *testing.T) { 90 | t.Parallel() 91 | type args struct { 92 | secret types.NamespacedName 93 | } 94 | 95 | testRefresherReconciler := RefresherReconciler{} 96 | testRefresherReconciler.SourceSecrets = []types.NamespacedName{ 97 | { 98 | Name: "testSecretName", 99 | Namespace: "testSecretNamespace", 100 | }, 101 | { 102 | Name: "testSecretName2", 103 | Namespace: "testSecretNamespace", 104 | }, 105 | { 106 | Name: "testSecretName3", 107 | Namespace: "testSecretNamespace2", 108 | }, 109 | } 110 | 111 | tests := []struct { 112 | name string 113 | refresherReconciler RefresherReconciler 114 | args args 115 | want bool 116 | }{ 117 | { 118 | name: "secret is found", 119 | refresherReconciler: testRefresherReconciler, 120 | args: args{ 121 | secret: types.NamespacedName{ 122 | Name: "testSecretName", 123 | Namespace: "testSecretNamespace", 124 | }, 125 | }, 126 | want: true, 127 | }, 128 | { 129 | name: "secret doesn't exist in the namespace", 130 | refresherReconciler: testRefresherReconciler, 131 | args: args{ 132 | secret: types.NamespacedName{ 133 | Name: "testSecretName2", 134 | Namespace: "testSecretNamespace2", 135 | }, 136 | }, 137 | want: false, 138 | }, 139 | { 140 | name: "secret name isn't on the list", 141 | refresherReconciler: testRefresherReconciler, 142 | args: args{ 143 | secret: types.NamespacedName{ 144 | Name: "testSecretName4", 145 | Namespace: "testSecretNamespace", 146 | }, 147 | }, 148 | want: false, 149 | }, 150 | { 151 | name: "secret name and namespace aren't on the list", 152 | refresherReconciler: testRefresherReconciler, 153 | args: args{ 154 | secret: types.NamespacedName{ 155 | Name: "testSecretName4", 156 | Namespace: "testSecretNamespace3", 157 | }, 158 | }, 159 | want: false, 160 | }, 161 | } 162 | for _, tt := range tests { 163 | tt := tt 164 | t.Run(tt.name, func(t *testing.T) { 165 | t.Parallel() 166 | found := tt.refresherReconciler.isSourceSecret(tt.args.secret) 167 | 168 | assert.Equal(t, tt.want, found) 169 | }) 170 | } 171 | } 172 | 173 | func TestRefresherReconciler_isMatchingSecret(t *testing.T) { 174 | t.Parallel() 175 | type args struct { 176 | obj client.Object 177 | } 178 | 179 | testRefresherReconciler := RefresherReconciler{} 180 | testRefresherReconciler.SourceSecrets = []types.NamespacedName{ 181 | { 182 | Name: "testSecretName", 183 | Namespace: "testSecretNamespace", 184 | }, 185 | } 186 | testRefresherReconciler.TargetSecret = types.NamespacedName{ 187 | Name: "testSecretName2", 188 | Namespace: "testSecretNamespace2", 189 | } 190 | testRefresherReconciler.Log = &testlogur.TestLogger{} 191 | 192 | tests := []struct { 193 | name string 194 | refresherReconciler RefresherReconciler 195 | args args 196 | want []ctrl.Request 197 | }{ 198 | { 199 | name: "object is a secret and is found a source secret", 200 | refresherReconciler: testRefresherReconciler, 201 | args: args{ 202 | obj: client.Object(&corev1.Secret{ 203 | TypeMeta: metav1.TypeMeta{ 204 | Kind: "Secret", 205 | }, 206 | ObjectMeta: metav1.ObjectMeta{ 207 | Name: "testSecretName", 208 | Namespace: "testSecretNamespace", 209 | }, 210 | }), 211 | }, 212 | want: []ctrl.Request{ 213 | { 214 | NamespacedName: testRefresherReconciler.TargetSecret, 215 | }, 216 | }, 217 | }, 218 | { 219 | name: "object is a secret and is found as a target secret", 220 | refresherReconciler: testRefresherReconciler, 221 | args: args{ 222 | obj: client.Object(&corev1.Secret{ 223 | TypeMeta: metav1.TypeMeta{ 224 | Kind: "Secret", 225 | }, 226 | ObjectMeta: metav1.ObjectMeta{ 227 | Name: "testSecretName2", 228 | Namespace: "testSecretNamespace2", 229 | }, 230 | }), 231 | }, 232 | want: []ctrl.Request{ 233 | { 234 | NamespacedName: testRefresherReconciler.TargetSecret, 235 | }, 236 | }, 237 | }, 238 | { 239 | name: "object isn't a secret", 240 | refresherReconciler: testRefresherReconciler, 241 | args: args{ 242 | obj: client.Object(&corev1.Pod{ 243 | TypeMeta: metav1.TypeMeta{ 244 | Kind: "Pod", 245 | }, 246 | ObjectMeta: metav1.ObjectMeta{ 247 | Name: "testSecretName", 248 | Namespace: "testSecretNamespace", 249 | }, 250 | }), 251 | }, 252 | want: []ctrl.Request{}, 253 | }, 254 | { 255 | name: "object is a secret but isn't found", 256 | refresherReconciler: testRefresherReconciler, 257 | args: args{ 258 | obj: client.Object(&corev1.Secret{ 259 | TypeMeta: metav1.TypeMeta{ 260 | Kind: "Secret", 261 | }, 262 | ObjectMeta: metav1.ObjectMeta{ 263 | Name: "testSecretName3", 264 | Namespace: "testSecretNamespace", 265 | }, 266 | }), 267 | }, 268 | want: []ctrl.Request{}, 269 | }, 270 | } 271 | for _, tt := range tests { 272 | tt := tt 273 | t.Run(tt.name, func(t *testing.T) { 274 | t.Parallel() 275 | found := tt.refresherReconciler.isMatchingSecret(tt.args.obj) 276 | 277 | assert.Equal(t, tt.want, found) 278 | }) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /controllers/stringset.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package controllers 16 | 17 | type StringSet []string 18 | 19 | func (s StringSet) Has(needle string) bool { 20 | for _, setMember := range s { 21 | if setMember == needle { 22 | return true 23 | } 24 | } 25 | 26 | return false 27 | } 28 | -------------------------------------------------------------------------------- /controllers/stringset_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStringSet_Has(t *testing.T) { 10 | t.Parallel() 11 | type args struct { 12 | needle string 13 | } 14 | 15 | haystack := StringSet{"testing1", "testing2", "testing3", "testing4", "testing5", "testing6", "testing7", "testing8"} 16 | 17 | tests := []struct { 18 | name string 19 | set StringSet 20 | args args 21 | want bool 22 | }{ 23 | { 24 | name: "positive", 25 | set: haystack, 26 | args: args{ 27 | needle: "testing3", 28 | }, 29 | want: true, 30 | }, 31 | { 32 | name: "negative", 33 | set: haystack, 34 | args: args{ 35 | needle: "testing9", 36 | }, 37 | want: false, 38 | }, 39 | } 40 | for _, tt := range tests { 41 | tt := tt 42 | t.Run(tt.name, func(t *testing.T) { 43 | t.Parallel() 44 | found := tt.set.Has(tt.args.needle) 45 | 46 | assert.Equal(t, tt.want, found) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /deploy/charts/embed.go: -------------------------------------------------------------------------------- 1 | package charts 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | ) 7 | 8 | var ( 9 | //go:embed imagepullsecrets imagepullsecrets/templates/_helpers.tpl 10 | imagePullSecretsEmbed embed.FS 11 | 12 | // ImagePullSecrets exposes the imagepullsecrets chart using relative file paths from the chart root 13 | ImagePullSecrets fs.FS 14 | ) 15 | 16 | func init() { 17 | var err error 18 | ImagePullSecrets, err = fs.Sub(imagePullSecretsEmbed, "imagepullsecrets") 19 | if err != nil { 20 | panic(err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /deploy/charts/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/banzaicloud/imps/deploy/charts 2 | 3 | go 1.17 -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: imagepullsecrets 3 | description: Dynamically manage pull secrets for images 4 | home: https://banzaicloud.com 5 | 6 | maintainers: 7 | - email: info@banzaicloud.com 8 | name: Banzai Cloud 9 | sources: 10 | - https://github.com/banzaicloud/backyards 11 | 12 | version: 0.3.13 13 | appVersion: 0.3.13 14 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/examples/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: imps-test 6 | 7 | --- 8 | apiVersion: v1 9 | kind: Secret 10 | metadata: 11 | name: imps-test-1 12 | namespace: imps-test 13 | stringData: 14 | test: "test12345" 15 | --- 16 | apiVersion: images.banzaicloud.io/v1alpha1 17 | kind: ImagePullSecret 18 | metadata: 19 | name: imps-test-1 20 | spec: 21 | target: 22 | secret: 23 | name: "image-pull-secret-banzai" 24 | namespaces: 25 | names: ["imps-test"] 26 | selectors: 27 | - matchExpressions: 28 | - key: "banzai-should-have-secrets" 29 | operator: "Exists" 30 | namespacesWithPods: 31 | - matchExpressions: 32 | - key: "banzai-should-have-secrets" 33 | operator: "Exists" 34 | registry: 35 | credentials: 36 | name: "imps-test-1" 37 | namespace: "imps-test" 38 | 39 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "imagepullsecret.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 7 | {{- end }} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "imagepullsecret.fullname" -}} 15 | {{- if .Values.fullnameOverride }} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 17 | {{- else }} 18 | {{- $name := default .Chart.Name .Values.nameOverride }} 19 | {{- if contains $name .Release.Name }} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 21 | {{- else }} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 23 | {{- end }} 24 | {{- end }} 25 | {{- end }} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "imagepullsecret.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 32 | {{- end }} 33 | 34 | 35 | {{- define "imagepullsecret-controller.fullname" -}} 36 | {{ include "imagepullsecret.fullname" . }}-controller 37 | {{- end }} 38 | 39 | {{- define "imagepullsecret-controller.name" -}} 40 | {{ include "imagepullsecret.name" . }}-controller 41 | {{- end }} 42 | 43 | {{- define "imagepullsecret-controller.labels" }} 44 | app: {{ include "imagepullsecret-controller.fullname" . }} 45 | app.kubernetes.io/name: {{ include "imagepullsecret-controller.name" . }} 46 | helm.sh/chart: {{ include "imagepullsecret.chart" . }} 47 | app.kubernetes.io/managed-by: {{ .Release.Service }} 48 | app.kubernetes.io/instance: {{ .Release.Name }} 49 | app.kubernetes.io/version: {{ .Chart.AppVersion | replace "+" "_" }} 50 | app.kubernetes.io/component: imagepullsecret-controller 51 | app.kubernetes.io/part-of: {{ include "imagepullsecret.name" . }} 52 | {{- end }} 53 | 54 | {{- define "imagepullsecret-controller.selectorLabels" -}} 55 | app.kubernetes.io/name: {{ include "imagepullsecret-controller.name" . }} 56 | app.kubernetes.io/instance: {{ .Release.Name }} 57 | {{- end }} 58 | 59 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/templates/default_imps_cr.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.defaultConfig.enabled }} 2 | --- 3 | apiVersion: images.banzaicloud.io/v1alpha1 4 | kind: ImagePullSecret 5 | metadata: 6 | name: {{ include "imagepullsecret-controller.fullname" . }}-default 7 | labels: 8 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 9 | spec: 10 | target: 11 | secret: 12 | name: {{ .Values.defaultConfig.targetSecretName }} 13 | {{- with .Values.defaultConfig.namespaces }} 14 | namespaces: 15 | {{ toYaml . | nindent 8 }} 16 | {{- end }} 17 | {{- with .Values.defaultConfig.namespacesWithPods }} 18 | namespacesWithPods: 19 | {{ toYaml . | nindent 8 }} 20 | {{- end }} 21 | registry: 22 | credentials: 23 | {{- with .Values.defaultConfig.credentials }} 24 | {{ toYaml . | nindent 8 }} 25 | {{- end }} 26 | {{- if .Values.defaultSecret.enabled }} 27 | - name: {{ include "imagepullsecret-controller.fullname" . }}-default 28 | namespace: {{ .Release.Namespace }} 29 | {{- end }} 30 | {{- end }} 31 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/templates/default_imps_secret.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.defaultSecret.enabled }} 2 | --- 3 | apiVersion: v1 4 | kind: Secret 5 | metadata: 6 | name: {{ include "imagepullsecret-controller.fullname" . }}-default 7 | namespace: {{ .Release.Namespace }} 8 | labels: 9 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 10 | type: {{ .Values.defaultSecret.type }} 11 | stringData: 12 | {{- with .Values.defaultSecret.stringData }} 13 | {{ toYaml . | nindent 4 }} 14 | {{- end }} 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "imagepullsecret-controller.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.replicas }} 10 | selector: 11 | matchLabels: 12 | {{- include "imagepullsecret-controller.selectorLabels" . | nindent 6 }} 13 | template: 14 | metadata: 15 | annotations: 16 | backyards.banzaicloud.io/scraping-interval: "{{ .Values.serviceMonitor.scrapeInterval }}" 17 | {{- if .Values.developmentMode.enabled }} 18 | backyards.banzaicloud.io/operator-release: "{{ .Values.operatorVersion }}" 19 | {{- else }} 20 | backyards.banzaicloud.io/operator-release: stable 21 | {{- end }} 22 | {{- if .Values.istio.revision }} 23 | istio.banzaicloud.io/rev: {{ .Values.istio.revision }} 24 | {{- end }} 25 | {{- with .Values.podAnnotations }} 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | labels: 29 | {{- include "imagepullsecret-controller.labels" . | nindent 8 }} 30 | spec: 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | serviceAccountName: {{ include "imagepullsecret-controller.fullname" . }} 34 | containers: 35 | - name: controller 36 | command: 37 | - /manager 38 | - --metrics-addr=:8080 39 | - --enable-leader-election 40 | - --config-namespace={{ .Release.Namespace }} 41 | securityContext: 42 | {{- toYaml .Values.securityContext | nindent 12 }} 43 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 44 | imagePullPolicy: {{ .Values.image.pullPolicy }} 45 | ports: 46 | - name: metrics 47 | containerPort: 8080 48 | protocol: TCP 49 | livenessProbe: 50 | httpGet: 51 | path: /metrics 52 | port: metrics 53 | readinessProbe: 54 | httpGet: 55 | path: /metrics 56 | port: metrics 57 | resources: 58 | {{- toYaml .Values.resources | nindent 12 }} 59 | env: 60 | {{- if .Values.log.level }} 61 | - name: LOG_LEVEL 62 | value: "{{ .Values.log.level }}" 63 | {{- end }} 64 | {{- with .Values.env }} 65 | {{- toYaml . | nindent 12 }} 66 | {{- end }} 67 | {{- with .Values.nodeSelector }} 68 | nodeSelector: 69 | {{- toYaml . | nindent 8 }} 70 | {{- end }} 71 | {{- with .Values.affinity }} 72 | affinity: 73 | {{- toYaml . | nindent 8 }} 74 | {{- end }} 75 | {{- with .Values.tolerations }} 76 | tolerations: 77 | {{- toYaml . | nindent 8 }} 78 | {{- end }} 79 | {{ with .Values.imagePullSecrets }} 80 | imagePullSecrets: 81 | {{- toYaml . | nindent 8 }} 82 | {{ end }} 83 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/templates/poddistruptionbudget.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.podDisruptionBudget.enabled }} 2 | apiVersion: policy/v1 3 | kind: PodDisruptionBudget 4 | metadata: 5 | name: {{ include "imagepullsecret-controller.fullname" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 9 | spec: 10 | minAvailable: 1 11 | selector: 12 | matchLabels: 13 | {{- include "imagepullsecret-controller.selectorLabels" . | nindent 6 }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "imagepullsecret-controller.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | 13 | --- 14 | apiVersion: rbac.authorization.k8s.io/v1 15 | kind: ClusterRole 16 | metadata: 17 | name: {{ include "imagepullsecret-controller.fullname" . }} 18 | labels: 19 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 20 | rules: 21 | - apiGroups: ["images.banzaicloud.io"] 22 | resources: ["*"] 23 | verbs: 24 | - get 25 | - list 26 | - watch 27 | - update 28 | - apiGroups: [""] 29 | resources: 30 | - configmaps 31 | - secrets 32 | - events 33 | verbs: 34 | - get 35 | - list 36 | - watch 37 | - create 38 | - update 39 | - delete 40 | - patch 41 | - apiGroups: [""] 42 | resources: 43 | - namespaces 44 | - pods 45 | verbs: 46 | - get 47 | - list 48 | - watch 49 | --- 50 | apiVersion: rbac.authorization.k8s.io/v1 51 | kind: Role 52 | metadata: 53 | name: {{ include "imagepullsecret-controller.fullname" . }} 54 | namespace: {{ .Release.Namespace }} 55 | labels: 56 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 57 | rules: 58 | - apiGroups: ["coordination.k8s.io"] 59 | resources: 60 | - leases 61 | verbs: 62 | - get 63 | - list 64 | - watch 65 | - create 66 | - update 67 | - delete 68 | --- 69 | apiVersion: rbac.authorization.k8s.io/v1 70 | kind: ClusterRoleBinding 71 | metadata: 72 | name: {{ include "imagepullsecret-controller.fullname" . }} 73 | labels: 74 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 75 | roleRef: 76 | kind: ClusterRole 77 | name: {{ include "imagepullsecret-controller.fullname" . }} 78 | apiGroup: rbac.authorization.k8s.io 79 | subjects: 80 | - kind: ServiceAccount 81 | name: {{ include "imagepullsecret-controller.fullname" . }} 82 | namespace: {{ .Release.Namespace }} 83 | --- 84 | apiVersion: rbac.authorization.k8s.io/v1 85 | kind: RoleBinding 86 | metadata: 87 | name: {{ include "imagepullsecret-controller.fullname" . }} 88 | namespace: {{ .Release.Namespace }} 89 | labels: 90 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 91 | roleRef: 92 | kind: Role 93 | name: {{ include "imagepullsecret-controller.fullname" . }} 94 | apiGroup: rbac.authorization.k8s.io 95 | subjects: 96 | - kind: ServiceAccount 97 | name: {{ include "imagepullsecret-controller.fullname" . }} 98 | namespace: {{ .Release.Namespace }} 99 | 100 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "imagepullsecret-controller.fullname" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "imagepullsecret-controller.labels" . | nindent 4 }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | ports: 11 | - port: {{ .Values.service.port }} 12 | targetPort: metrics 13 | protocol: TCP 14 | name: http-metrics 15 | selector: 16 | {{- include "imagepullsecret-controller.selectorLabels" . | nindent 4 }} 17 | -------------------------------------------------------------------------------- /deploy/charts/imagepullsecrets/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for imagepullsecret-controller 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicas: 1 6 | 7 | istio: 8 | revision: "" 9 | 10 | podAnnotations: {} 11 | 12 | podSecurityContext: 13 | runAsNonRoot: true 14 | seccompProfile: 15 | type: RuntimeDefault 16 | securityContext: 17 | allowPrivilegeEscalation: false 18 | capabilities: 19 | drop: 20 | - ALL 21 | image: 22 | repository: ghcr.io/banzaicloud/imagepullsecrets 23 | tag: v0.3.13 24 | pullPolicy: IfNotPresent 25 | 26 | imagePullSecrets: [] 27 | 28 | nodeSelector: {} 29 | affinity: {} 30 | tolerations: [] 31 | resources: 32 | requests: 33 | memory: "100Mi" 34 | cpu: "100m" 35 | limits: 36 | memory: "200Mi" 37 | cpu: "300m" 38 | 39 | service: 40 | type: ClusterIP 41 | port: 8080 42 | 43 | serviceAccount: 44 | annotations: {} 45 | 46 | serviceMonitor: 47 | scrapeInterval: 5s 48 | tlsConfig: {} 49 | 50 | developmentMode: 51 | enabled: false 52 | 53 | podDisruptionBudget: 54 | enabled: false 55 | 56 | log: 57 | {} 58 | # level: info # accepted values: panic, fatal, error, warn, warning, info, debug, trace 59 | 60 | # additional environment variables to be injected into the container 61 | env: 62 | {} 63 | 64 | 65 | defaultConfig: 66 | enabled: false 67 | targetSecretName: default-secret-name 68 | namespaces: {} 69 | namespacesWithPods: [] 70 | credentials: [] 71 | 72 | defaultSecret: 73 | enabled: false 74 | stringData: {} 75 | type: "" 76 | 77 | -------------------------------------------------------------------------------- /docs/RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release procedure 2 | 3 | As of now we don't have an automated release process. This guide shows how 4 | to do a new release of IMPS. 5 | 6 | Steps: 7 | - Update deploy/charts/imagepullsecrets/Chart.yaml with the new target version 8 | - Update deploy/charts/imagepullsecrets/values.yaml with the new target version 9 | (image name) 10 | - Execute a make build, so that all generated files are updated 11 | - Commit the change to main (please raise a PR) 12 | - Add and push the following *annotated* tags and push them seperately (circleci ignores 13 | tags when multiple tags are pushed): 14 | - vX.Y.Z: releases new version from Docker image, main go.mod 15 | - deploy/charts/vX.Y.Z: releases a new version from the charts module 16 | - chart/imagepullsecrets/X.Y.Z: release a new version from the chart 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/banzaicloud/imps 2 | 3 | go 1.18 4 | 5 | require ( 6 | emperror.dev/emperror v0.33.0 7 | emperror.dev/errors v0.8.1 8 | emperror.dev/handler/logur v0.5.0 9 | github.com/aws/aws-sdk-go-v2 v1.17.8 10 | github.com/aws/aws-sdk-go-v2/credentials v1.10.0 11 | github.com/aws/aws-sdk-go-v2/service/ecr v1.1.1 12 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.9 13 | github.com/banzaicloud/imps/api v0.0.1 14 | github.com/cisco-open/operator-tools v0.29.0 15 | github.com/onsi/ginkgo/v2 v2.1.4 16 | github.com/onsi/gomega v1.19.0 17 | github.com/sirupsen/logrus v1.8.1 18 | github.com/spf13/pflag v1.0.5 19 | github.com/spf13/viper v1.7.0 20 | github.com/stretchr/testify v1.7.2 21 | gotest.tools v2.2.0+incompatible 22 | k8s.io/api v0.24.2 23 | k8s.io/apimachinery v0.24.2 24 | k8s.io/client-go v0.24.2 25 | logur.dev/adapter/logrus v0.5.0 26 | logur.dev/integration/logr v0.5.0 27 | logur.dev/logur v0.17.0 28 | sigs.k8s.io/controller-runtime v0.11.0 29 | ) 30 | 31 | require ( 32 | github.com/PuerkitoBio/purell v1.1.1 // indirect 33 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 34 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.32 // indirect 35 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.26 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.26 // indirect 37 | github.com/aws/smithy-go v1.13.5 // indirect 38 | github.com/beorn7/perks v1.0.1 // indirect 39 | github.com/briandowns/spinner v1.12.0 // indirect 40 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 41 | github.com/cisco-open/k8s-objectmatcher v1.9.0 // indirect 42 | github.com/cppforlife/go-patch v0.2.0 // indirect 43 | github.com/davecgh/go-spew v1.1.1 // indirect 44 | github.com/emicklei/go-restful/v3 v3.8.0 // indirect 45 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 46 | github.com/fatih/color v1.13.0 // indirect 47 | github.com/fsnotify/fsnotify v1.5.1 // indirect 48 | github.com/go-logr/logr v1.2.2 // indirect 49 | github.com/go-logr/zapr v1.2.0 // indirect 50 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 51 | github.com/go-openapi/jsonreference v0.19.5 // indirect 52 | github.com/go-openapi/swag v0.19.14 // indirect 53 | github.com/gogo/protobuf v1.3.2 // indirect 54 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 55 | github.com/golang/protobuf v1.5.2 // indirect 56 | github.com/google/gnostic v0.5.7-v3refs // indirect 57 | github.com/google/go-cmp v0.5.8 // indirect 58 | github.com/google/gofuzz v1.2.0 // indirect 59 | github.com/google/uuid v1.3.0 // indirect 60 | github.com/hashicorp/hcl v1.0.0 // indirect 61 | github.com/iancoleman/orderedmap v0.2.0 // indirect 62 | github.com/imdario/mergo v0.3.12 // indirect 63 | github.com/jmespath/go-jmespath v0.4.0 // indirect 64 | github.com/josharian/intern v1.0.0 // indirect 65 | github.com/json-iterator/go v1.1.12 // indirect 66 | github.com/magiconair/properties v1.8.1 // indirect 67 | github.com/mailru/easyjson v0.7.6 // indirect 68 | github.com/mattn/go-colorable v0.1.12 // indirect 69 | github.com/mattn/go-isatty v0.0.14 // indirect 70 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 71 | github.com/mitchellh/mapstructure v1.4.1 // indirect 72 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 73 | github.com/modern-go/reflect2 v1.0.2 // indirect 74 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 75 | github.com/pelletier/go-toml v1.2.0 // indirect 76 | github.com/pkg/errors v0.9.1 // indirect 77 | github.com/pmezard/go-difflib v1.0.0 // indirect 78 | github.com/prometheus/client_golang v1.12.1 // indirect 79 | github.com/prometheus/client_model v0.2.0 // indirect 80 | github.com/prometheus/common v0.32.1 // indirect 81 | github.com/prometheus/procfs v0.7.3 // indirect 82 | github.com/spf13/afero v1.6.0 // indirect 83 | github.com/spf13/cast v1.4.1 // indirect 84 | github.com/spf13/jwalterweatherman v1.0.0 // indirect 85 | github.com/stretchr/objx v0.1.1 // indirect 86 | github.com/subosito/gotenv v1.2.0 // indirect 87 | github.com/wayneashleyberry/terminal-dimensions v1.0.0 // indirect 88 | go.uber.org/atomic v1.7.0 // indirect 89 | go.uber.org/multierr v1.6.0 // indirect 90 | go.uber.org/zap v1.19.1 // indirect 91 | golang.org/x/net v0.7.0 // indirect 92 | golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect 93 | golang.org/x/sys v0.5.0 // indirect 94 | golang.org/x/term v0.5.0 // indirect 95 | golang.org/x/text v0.7.0 // indirect 96 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 97 | gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect 98 | google.golang.org/appengine v1.6.7 // indirect 99 | google.golang.org/protobuf v1.27.1 // indirect 100 | gopkg.in/inf.v0 v0.9.1 // indirect 101 | gopkg.in/ini.v1 v1.51.0 // indirect 102 | gopkg.in/yaml.v2 v2.4.0 // indirect 103 | gopkg.in/yaml.v3 v3.0.1 // indirect 104 | k8s.io/apiextensions-apiserver v0.24.2 // indirect 105 | k8s.io/component-base v0.24.2 // indirect 106 | k8s.io/klog/v2 v2.60.1 // indirect 107 | k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect 108 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect 109 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 110 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 111 | sigs.k8s.io/yaml v1.3.0 // indirect 112 | ) 113 | 114 | replace github.com/banzaicloud/imps/api => ./api 115 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. -------------------------------------------------------------------------------- /internal/cron/reconcile.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cron 16 | 17 | import ( 18 | "time" 19 | 20 | ctrl "sigs.k8s.io/controller-runtime" 21 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 22 | ) 23 | 24 | func EnsurePeriodicReconcile(interval time.Duration, result reconcile.Result, err error) (reconcile.Result, error) { 25 | if err != nil { 26 | return result, err 27 | } 28 | 29 | return ctrl.Result{ 30 | Requeue: true, 31 | RequeueAfter: calculateRequeueAfter(result, interval), 32 | }, err 33 | } 34 | 35 | func calculateRequeueAfter(result reconcile.Result, periodicReconcileInterval time.Duration) time.Duration { 36 | if result.IsZero() { 37 | return periodicReconcileInterval 38 | } 39 | 40 | if result.RequeueAfter < periodicReconcileInterval { 41 | return result.RequeueAfter 42 | } 43 | 44 | return periodicReconcileInterval 45 | } 46 | -------------------------------------------------------------------------------- /internal/cron/reconcile_test.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "emperror.dev/errors" 8 | "gotest.tools/assert" 9 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 10 | ) 11 | 12 | func TestCron_calculateRequeueAfter(t *testing.T) { 13 | t.Parallel() 14 | type args struct { 15 | result reconcile.Result 16 | periodicReconcileInterval time.Duration 17 | } 18 | 19 | tests := []struct { 20 | name string 21 | args args 22 | want time.Duration 23 | }{ 24 | { 25 | name: "result is zero", 26 | args: args{ 27 | result: reconcile.Result{}, 28 | periodicReconcileInterval: 5 * time.Second, 29 | }, 30 | want: 5 * time.Second, 31 | }, 32 | { 33 | name: "requeue after is smaller than periodicReconcileInterval", 34 | args: args{ 35 | result: reconcile.Result{ 36 | RequeueAfter: 3 * time.Second, 37 | }, 38 | periodicReconcileInterval: 5 * time.Second, 39 | }, 40 | want: 3 * time.Second, 41 | }, 42 | { 43 | name: "requeue after is greater than periodicReconcileInterval", 44 | args: args{ 45 | result: reconcile.Result{ 46 | RequeueAfter: 7 * time.Second, 47 | }, 48 | periodicReconcileInterval: 5 * time.Second, 49 | }, 50 | want: 5 * time.Second, 51 | }, 52 | } 53 | for _, tt := range tests { 54 | tt := tt 55 | t.Run(tt.name, func(t *testing.T) { 56 | t.Parallel() 57 | found := calculateRequeueAfter(tt.args.result, tt.args.periodicReconcileInterval) 58 | 59 | assert.Equal(t, tt.want, found) 60 | }) 61 | } 62 | } 63 | 64 | func TestCron_EnsurePeriodicReconcile(t *testing.T) { 65 | t.Parallel() 66 | type args struct { 67 | interval time.Duration 68 | result reconcile.Result 69 | err error 70 | } 71 | 72 | tests := []struct { 73 | name string 74 | args args 75 | want reconcile.Result 76 | expectedErr error 77 | }{ 78 | { 79 | name: "basic functionality test", 80 | args: args{ 81 | interval: 5 * time.Second, 82 | result: reconcile.Result{}, 83 | err: nil, 84 | }, 85 | want: reconcile.Result{ 86 | Requeue: true, 87 | RequeueAfter: 5 * time.Second, 88 | }, 89 | expectedErr: nil, 90 | }, 91 | { 92 | name: "error is not nil", 93 | args: args{ 94 | interval: 5 * time.Second, 95 | result: reconcile.Result{}, 96 | err: errors.New("test"), 97 | }, 98 | want: reconcile.Result{ 99 | Requeue: false, 100 | RequeueAfter: 0 * time.Second, 101 | }, 102 | expectedErr: errors.New("test"), 103 | }, 104 | } 105 | for _, tt := range tests { 106 | tt := tt 107 | t.Run(tt.name, func(t *testing.T) { 108 | t.Parallel() 109 | found, err := EnsurePeriodicReconcile(tt.args.interval, tt.args.result, tt.args.err) 110 | 111 | assert.Equal(t, tt.want, found) 112 | if tt.expectedErr != nil { 113 | assert.Error(t, err, tt.expectedErr.Error()) 114 | } 115 | }) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /internal/errorhandler/errorhandler.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errorhandler 16 | 17 | import ( 18 | "emperror.dev/emperror" 19 | logurhandler "emperror.dev/handler/logur" 20 | ) 21 | 22 | // New returns a new error handler. 23 | func New(logger logurhandler.ErrorLogger) emperror.ErrorHandler { 24 | return logurhandler.WithStackTrace(logurhandler.New(logger)) 25 | } 26 | -------------------------------------------------------------------------------- /internal/log/config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package log 16 | 17 | import ( 18 | "emperror.dev/errors" 19 | "github.com/spf13/pflag" 20 | "github.com/spf13/viper" 21 | ) 22 | 23 | // Config holds details necessary for logging. 24 | type Config struct { 25 | // Format specifies the output log format. 26 | // Accepted values are: json, logfmt 27 | Format string `json:"format,omitempty" mapstructure:"format"` 28 | 29 | // Level is the minimum log level that should appear on the output. 30 | Level string `json:"level,omitempty" mapstructure:"level"` 31 | 32 | // NoColor makes sure that no log output gets colorized. 33 | NoColor bool `json:"noColor,omitempty" mapstructure:"noColor"` 34 | } 35 | 36 | // Validate validates the configuration. 37 | func (c Config) Validate() (Config, error) { 38 | if c.Format == "" { 39 | c.Format = "logfmt" 40 | } 41 | 42 | if c.Format != "json" && c.Format != "logfmt" { 43 | return c, errors.New("invalid log format: " + c.Format) 44 | } 45 | 46 | return c, nil 47 | } 48 | 49 | func ConfigureLoggingFlags(v *viper.Viper, p *pflag.FlagSet) { 50 | v.SetDefault("log.format", "json") 51 | p.String("log.level", "info", "Log level") 52 | v.SetDefault("log.level", "info") 53 | v.RegisterAlias("log.noColor", "no_color") 54 | } 55 | -------------------------------------------------------------------------------- /internal/log/config_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | 6 | "emperror.dev/errors" 7 | "gotest.tools/assert" 8 | ) 9 | 10 | func TestConfig_Validate(t *testing.T) { 11 | t.Parallel() 12 | tests := []struct { 13 | name string 14 | config Config 15 | want Config 16 | expectedErr error 17 | }{ 18 | { 19 | name: "format string is empty", 20 | config: Config{}, 21 | want: Config{ 22 | Format: "logfmt", 23 | }, 24 | expectedErr: nil, 25 | }, 26 | { 27 | name: "format string is good", 28 | config: Config{ 29 | Format: "json", 30 | }, 31 | want: Config{ 32 | Format: "json", 33 | }, 34 | expectedErr: nil, 35 | }, 36 | { 37 | name: "format string is wrong", 38 | config: Config{ 39 | Format: "wrongFormat", 40 | }, 41 | want: Config{ 42 | Format: "wrongFormat", 43 | }, 44 | expectedErr: errors.New("invalid log format: wrongFormat"), 45 | }, 46 | } 47 | for _, tt := range tests { 48 | tt := tt 49 | t.Run(tt.name, func(t *testing.T) { 50 | t.Parallel() 51 | found, err := tt.config.Validate() 52 | 53 | assert.Equal(t, tt.want, found) 54 | if tt.expectedErr != nil { 55 | assert.Error(t, err, tt.expectedErr.Error()) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/log/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package log configures a new logger for an application. 16 | package log 17 | 18 | import ( 19 | "os" 20 | 21 | "github.com/sirupsen/logrus" 22 | logrusadapter "logur.dev/adapter/logrus" 23 | "logur.dev/logur" 24 | ) 25 | 26 | // NewLogger creates a new logger. 27 | func NewLogger(config Config) logur.Logger { 28 | logger := logrus.New() 29 | 30 | logger.SetOutput(os.Stdout) 31 | logger.SetFormatter(&logrus.TextFormatter{ 32 | DisableColors: config.NoColor, 33 | EnvironmentOverrideColors: true, 34 | }) 35 | 36 | switch config.Format { 37 | case "logfmt": 38 | // Already the default 39 | 40 | case "json": 41 | logger.SetFormatter(&logrus.JSONFormatter{}) 42 | } 43 | 44 | if level, err := logrus.ParseLevel(config.Level); err == nil { 45 | logger.SetLevel(level) 46 | } 47 | 48 | return logrusadapter.New(logger) 49 | } 50 | 51 | // WithFields returns a new contextual logger instance with context added to it. 52 | func WithFields(logger logur.Logger, fields map[string]interface{}) logur.Logger { 53 | return logur.WithFields(logger, fields) 54 | } 55 | -------------------------------------------------------------------------------- /internal/log/standard_logger.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package log 16 | 17 | import ( 18 | "log" 19 | 20 | "logur.dev/logur" 21 | ) 22 | 23 | // NewErrorStandardLogger returns a new standard logger logging on error level. 24 | func NewErrorStandardLogger(logger logur.Logger) *log.Logger { 25 | return logur.NewErrorStandardLogger(logger, "", 0) 26 | } 27 | 28 | // SetStandardLogger sets the global logger's output to a custom logger instance. 29 | func SetStandardLogger(logger logur.Logger) { 30 | log.SetOutput(logur.NewLevelWriter(logger, logur.Info)) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/ecr/manager.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ecr 16 | 17 | import ( 18 | "context" 19 | "sync" 20 | "time" 21 | 22 | "emperror.dev/errors" 23 | "github.com/aws/aws-sdk-go-v2/aws" 24 | ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types" 25 | "logur.dev/logur" 26 | ) 27 | 28 | const ( 29 | discardUnqueriedTokensAfter = 60 * time.Minute 30 | ) 31 | 32 | type TokenManager struct { 33 | sync.Mutex 34 | ManagedTokens map[string]*Token 35 | Logger logur.Logger 36 | } 37 | 38 | func Initialize(logger logur.Logger) { 39 | tokenManager = NewECRTokenManager(logger) 40 | tokenManager.start() 41 | } 42 | 43 | var tokenManager *TokenManager 44 | 45 | func GetAuthorizationToken(ctx context.Context, region string, credentials aws.Credentials, roleArn string, client ClientInterface) (ecrTypes.AuthorizationData, error) { 46 | return tokenManager.GetAuthorizationToken(ctx, StringableCredentials{ 47 | Credentials: credentials, 48 | Region: region, 49 | RoleArn: roleArn, 50 | }, client) 51 | } 52 | 53 | func NewECRTokenManager(logger logur.Logger) *TokenManager { 54 | return &TokenManager{ 55 | ManagedTokens: map[string]*Token{}, 56 | Logger: logger, 57 | } 58 | } 59 | 60 | func (t *TokenManager) start() { 61 | go t.tokenUpdater() 62 | } 63 | 64 | func (t *TokenManager) tokenUpdater() { 65 | c := time.NewTicker(5 * time.Second) 66 | for range c.C { 67 | t.updateTokens() 68 | t.discardOldTokens() 69 | } 70 | } 71 | 72 | func (t *TokenManager) updateTokens() { 73 | t.Lock() 74 | defer t.Unlock() 75 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 76 | defer cancel() 77 | for _, token := range t.ManagedTokens { 78 | if token.CurrentToken == nil || time.Until(*token.CurrentToken.ExpiresAt) < token.TokenValidityDuration/2 { 79 | err := token.Refresh(ctx) 80 | if err != nil { 81 | t.Logger.Warn("error refreshing token", map[string]interface{}{ 82 | "error": err, 83 | "aws_access_key_id": token.Creds.AccessKeyID, 84 | "region": token.Creds.Region, 85 | }) 86 | } else { 87 | t.Logger.Info("token refreshed", map[string]interface{}{ 88 | "aws_access_key_id": token.Creds.AccessKeyID, 89 | "region": token.Creds.Region, 90 | }) 91 | } 92 | } 93 | } 94 | } 95 | 96 | func (t *TokenManager) discardOldTokens() { 97 | t.Lock() 98 | defer t.Unlock() 99 | 100 | for id, token := range t.ManagedTokens { 101 | if time.Since(token.LastQueriedAt) > discardUnqueriedTokensAfter { 102 | delete(t.ManagedTokens, id) 103 | } 104 | } 105 | } 106 | 107 | func (t *TokenManager) GetAuthorizationToken(ctx context.Context, key StringableCredentials, client ClientInterface) (ecrTypes.AuthorizationData, error) { 108 | t.Lock() 109 | defer t.Unlock() 110 | token, found := t.ManagedTokens[key.String()] 111 | if !found { 112 | token, err := NewECRToken(ctx, key, client) 113 | if err != nil { 114 | return ecrTypes.AuthorizationData{}, err 115 | } 116 | t.ManagedTokens[key.String()] = token 117 | if token.CurrentToken == nil { 118 | return ecrTypes.AuthorizationData{}, errors.New("no token is available") 119 | } 120 | t.Logger.Info("token refreshed", map[string]interface{}{ 121 | "aws_access_key_id": token.Creds.AccessKeyID, 122 | "region": token.Creds.Region, 123 | }) 124 | 125 | return *token.CurrentToken, nil 126 | } 127 | 128 | token.LastQueriedAt = time.Now() 129 | 130 | return *token.CurrentToken, nil 131 | } 132 | -------------------------------------------------------------------------------- /pkg/ecr/manager_test.go: -------------------------------------------------------------------------------- 1 | package ecr 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "emperror.dev/errors" 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/ecr" 11 | "github.com/aws/aws-sdk-go-v2/service/ecr/types" 12 | "github.com/stretchr/testify/mock" 13 | "gotest.tools/assert" 14 | "logur.dev/logur" 15 | ) 16 | 17 | func TestTokenManager_GetAuthorizationToken(t *testing.T) { 18 | t.Parallel() 19 | type args struct { 20 | key StringableCredentials 21 | client ClientInterface 22 | } 23 | 24 | testTokenName := "testToken" 25 | 26 | tests := []struct { 27 | name string 28 | args args 29 | mockTokenOutput *ecr.GetAuthorizationTokenOutput 30 | tokenManager *TokenManager 31 | wanted types.AuthorizationData 32 | expectedErr error 33 | }{ 34 | { 35 | name: "basic functionality test", 36 | args: args{ 37 | key: StringableCredentials{ 38 | aws.Credentials{ 39 | AccessKeyID: "testAccessKeyID", 40 | }, 41 | "testRegion", 42 | "testRole", 43 | }, 44 | client: &MockECRClient{}, 45 | }, 46 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 47 | AuthorizationData: []types.AuthorizationData{ 48 | { 49 | AuthorizationToken: &testTokenName, 50 | }, 51 | }, 52 | }, 53 | tokenManager: &TokenManager{ 54 | ManagedTokens: map[string]*Token{}, 55 | Logger: &logur.TestLogger{}, 56 | }, 57 | wanted: types.AuthorizationData{ 58 | AuthorizationToken: &testTokenName, 59 | }, 60 | expectedErr: nil, 61 | }, 62 | { 63 | name: "error from NewECRToken", 64 | args: args{ 65 | key: StringableCredentials{ 66 | aws.Credentials{ 67 | AccessKeyID: "testAccessKeyID", 68 | }, 69 | "testRegion", 70 | "testRole", 71 | }, 72 | client: &MockECRClient{}, 73 | }, 74 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 75 | AuthorizationData: []types.AuthorizationData{}, 76 | }, 77 | tokenManager: &TokenManager{ 78 | ManagedTokens: map[string]*Token{}, 79 | Logger: &logur.TestLogger{}, 80 | }, 81 | wanted: types.AuthorizationData{}, 82 | expectedErr: errors.New("no authorization data is returned from ECR"), 83 | }, 84 | } 85 | for _, tt := range tests { 86 | tt := tt 87 | t.Run(tt.name, func(t *testing.T) { 88 | t.Parallel() 89 | mockClient := &MockECRClient{} 90 | mockClient.On("GetAuthorizationToken", mock.Anything, mock.Anything).Return(tt.mockTokenOutput, nil) 91 | 92 | found, err := tt.tokenManager.GetAuthorizationToken(context.Background(), tt.args.key, mockClient) 93 | 94 | if tt.expectedErr != nil { 95 | assert.Equal(t, tt.expectedErr.Error(), err.Error()) 96 | } else { 97 | assert.DeepEqual(t, tt.wanted, found) 98 | assert.NilError(t, err) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestTokenManager_discardOldTokens(t *testing.T) { 105 | t.Parallel() 106 | currentTime := time.Now() 107 | 108 | tests := []struct { 109 | name string 110 | tokenManager *TokenManager 111 | }{ 112 | { 113 | name: "basic functionality test", 114 | tokenManager: &TokenManager{ 115 | ManagedTokens: map[string]*Token{ 116 | "tokenToDiscard": { 117 | LastQueriedAt: currentTime.Add(-70 * time.Minute), 118 | }, 119 | "tokenNotToDiscard": { 120 | LastQueriedAt: currentTime.Add(-40 * time.Minute), 121 | }, 122 | }, 123 | }, 124 | }, 125 | } 126 | for _, tt := range tests { 127 | tt := tt 128 | t.Run(tt.name, func(t *testing.T) { 129 | t.Parallel() 130 | tt.tokenManager.discardOldTokens() 131 | 132 | assert.DeepEqual(t, tt.tokenManager.ManagedTokens, map[string]*Token{ 133 | "tokenNotToDiscard": { 134 | LastQueriedAt: currentTime.Add(-40 * time.Minute), 135 | }, 136 | }) 137 | }) 138 | } 139 | } 140 | 141 | func TestTokenManager_updateTokens(t *testing.T) { 142 | t.Parallel() 143 | testTokenName := "testToken" 144 | expiryTime := time.Now().Add(5 * time.Minute) 145 | newExpiryTime := expiryTime.Add(5 * time.Minute) 146 | 147 | tests := []struct { 148 | name string 149 | mockTokenOutput *ecr.GetAuthorizationTokenOutput 150 | tokenManager *TokenManager 151 | }{ 152 | { 153 | name: "currentToken is nil", 154 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 155 | AuthorizationData: []types.AuthorizationData{ 156 | { 157 | AuthorizationToken: &testTokenName, 158 | ExpiresAt: &newExpiryTime, 159 | }, 160 | }, 161 | }, 162 | tokenManager: &TokenManager{ 163 | ManagedTokens: map[string]*Token{ 164 | "testName": { 165 | CurrentToken: nil, 166 | }, 167 | }, 168 | Logger: &logur.TestLogger{}, 169 | }, 170 | }, 171 | { 172 | name: "less than half of the validity duration is left", 173 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 174 | AuthorizationData: []types.AuthorizationData{ 175 | { 176 | AuthorizationToken: &testTokenName, 177 | ExpiresAt: &newExpiryTime, 178 | }, 179 | }, 180 | }, 181 | tokenManager: &TokenManager{ 182 | ManagedTokens: map[string]*Token{ 183 | "testName": { 184 | CurrentToken: &types.AuthorizationData{ 185 | AuthorizationToken: &testTokenName, 186 | ExpiresAt: &expiryTime, 187 | }, 188 | TokenValidityDuration: 10 * time.Minute, 189 | }, 190 | }, 191 | Logger: &logur.TestLogger{}, 192 | }, 193 | }, 194 | } 195 | for _, tt := range tests { 196 | tt := tt 197 | t.Run(tt.name, func(t *testing.T) { 198 | t.Parallel() 199 | mockClient := &MockECRClient{} 200 | mockClient.On("GetAuthorizationToken", mock.Anything, mock.Anything).Return(tt.mockTokenOutput, nil) 201 | tt.tokenManager.ManagedTokens["testName"].Client = mockClient 202 | 203 | tt.tokenManager.updateTokens() 204 | 205 | assert.Equal(t, len(tt.tokenManager.ManagedTokens), 1) 206 | assert.DeepEqual(t, tt.tokenManager.ManagedTokens["testName"].CurrentToken, &types.AuthorizationData{ 207 | AuthorizationToken: &testTokenName, 208 | ExpiresAt: &newExpiryTime, 209 | }) 210 | }) 211 | } 212 | } 213 | 214 | func TestTokenManager_NewECRTokenManager(t *testing.T) { 215 | t.Parallel() 216 | type args struct { 217 | logger logur.Logger 218 | } 219 | 220 | tests := []struct { 221 | name string 222 | args args 223 | want *TokenManager 224 | }{ 225 | { 226 | name: "basic functionality test", 227 | args: args{ 228 | logger: &logur.TestLogger{}, 229 | }, 230 | want: &TokenManager{ 231 | ManagedTokens: map[string]*Token{}, 232 | Logger: &logur.TestLogger{}, 233 | }, 234 | }, 235 | } 236 | for _, tt := range tests { 237 | tt := tt 238 | t.Run(tt.name, func(t *testing.T) { 239 | t.Parallel() 240 | found := NewECRTokenManager(tt.args.logger) 241 | 242 | assert.Equal(t, len(tt.want.ManagedTokens), len(found.ManagedTokens)) 243 | }) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /pkg/ecr/token.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ecr 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "emperror.dev/errors" 22 | "github.com/aws/aws-sdk-go-v2/service/ecr" 23 | ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types" 24 | ) 25 | 26 | const ( 27 | // assumedTokenValidityTime specifies how long to consider the returned token to be valid if not specified in 28 | // the response 29 | assumedTokenValidityTime = 20 * time.Minute 30 | ) 31 | 32 | type ClientInterface interface { 33 | GetAuthorizationToken(ctx context.Context, input *ecr.GetAuthorizationTokenInput, _ ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) 34 | } 35 | 36 | type Token struct { 37 | Creds StringableCredentials 38 | CurrentToken *ecrTypes.AuthorizationData 39 | TokenValidityDuration time.Duration 40 | LastQueriedAt time.Time 41 | Client ClientInterface 42 | } 43 | 44 | func NewECRToken(ctx context.Context, creds StringableCredentials, client ClientInterface) (*Token, error) { 45 | token := &Token{ 46 | Creds: creds, 47 | Client: client, 48 | } 49 | 50 | err := token.Refresh(ctx) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return token, nil 56 | } 57 | 58 | func (t *Token) Refresh(ctx context.Context) error { 59 | if t.Client == nil { 60 | t.Client = ecr.NewFromConfig(t.Creds.ToAwsConfig()) 61 | } 62 | 63 | // note: RegistryIds is deprecated, any account's registries can be accessed via the returned token 64 | authToken, err := t.Client.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if len(authToken.AuthorizationData) == 0 { 70 | return errors.New("no authorization data is returned from ECR") 71 | } 72 | 73 | if len(authToken.AuthorizationData) > 1 { 74 | // This should never happen according to current API specs 75 | return errors.NewWithDetails("multiple authorization records are returned for ECR", "response", authToken) 76 | } 77 | 78 | if authToken.AuthorizationData[0].AuthorizationToken == nil { 79 | return errors.New("no authorization data is returned from ECR - authorization token is empty") 80 | } 81 | 82 | fetchedToken := authToken.AuthorizationData[0] 83 | t.CurrentToken = &fetchedToken 84 | if fetchedToken.ExpiresAt != nil { 85 | t.TokenValidityDuration = time.Until(*fetchedToken.ExpiresAt) 86 | } else { 87 | t.TokenValidityDuration = assumedTokenValidityTime 88 | } 89 | 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/ecr/token_test.go: -------------------------------------------------------------------------------- 1 | package ecr 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "emperror.dev/errors" 8 | "github.com/aws/aws-sdk-go-v2/service/ecr" 9 | "github.com/aws/aws-sdk-go-v2/service/ecr/types" 10 | "github.com/stretchr/testify/mock" 11 | "gotest.tools/assert" 12 | ) 13 | 14 | type MockECRClient struct { 15 | mock.Mock 16 | } 17 | 18 | func (m *MockECRClient) GetAuthorizationToken(ctx context.Context, input *ecr.GetAuthorizationTokenInput, _ ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) { 19 | args := m.Called(ctx, input) 20 | // nolint:forcetypeassert 21 | return args.Get(0).(*ecr.GetAuthorizationTokenOutput), args.Error(1) 22 | } 23 | 24 | func TestToken_NewECRToken(t *testing.T) { 25 | t.Parallel() 26 | type args struct { 27 | creds StringableCredentials 28 | client ClientInterface 29 | } 30 | 31 | mockClient := &MockECRClient{} 32 | testTokenName := "testToken" 33 | mockTokenOutput := &ecr.GetAuthorizationTokenOutput{ 34 | AuthorizationData: []types.AuthorizationData{ 35 | { 36 | AuthorizationToken: &testTokenName, 37 | }, 38 | }, 39 | } 40 | mockClient.On("GetAuthorizationToken", mock.Anything, mock.Anything).Return(mockTokenOutput, nil) 41 | 42 | tests := []struct { 43 | name string 44 | args args 45 | want *Token 46 | expectedErr error 47 | }{ 48 | { 49 | name: "basic functionality test", 50 | args: args{ 51 | creds: StringableCredentials{}, 52 | client: mockClient, 53 | }, 54 | want: &Token{ 55 | CurrentToken: &types.AuthorizationData{ 56 | AuthorizationToken: &testTokenName, 57 | }, 58 | }, 59 | expectedErr: nil, 60 | }, 61 | { 62 | name: "no token returned", 63 | args: args{ 64 | creds: StringableCredentials{}, 65 | client: nil, 66 | }, 67 | want: &Token{}, 68 | expectedErr: errors.New("operation error ECR: GetAuthorizationToken, failed to resolve service endpoint, an AWS region is required, but was not found"), 69 | }, 70 | } 71 | for _, tt := range tests { 72 | tt := tt 73 | t.Run(tt.name, func(t *testing.T) { 74 | t.Parallel() 75 | found, err := NewECRToken(context.Background(), tt.args.creds, tt.args.client) 76 | 77 | if tt.expectedErr != nil { 78 | assert.Equal(t, tt.expectedErr.Error(), err.Error()) 79 | } else { 80 | assert.DeepEqual(t, tt.want.CurrentToken, found.CurrentToken) 81 | assert.NilError(t, err) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestToken_Refresh(t *testing.T) { 88 | t.Parallel() 89 | 90 | testTokenName := "testToken" 91 | 92 | tests := []struct { 93 | name string 94 | mockTokenOutput *ecr.GetAuthorizationTokenOutput 95 | token *Token 96 | expectedErr error 97 | }{ 98 | { 99 | name: "basic functionality test", 100 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 101 | AuthorizationData: []types.AuthorizationData{ 102 | { 103 | AuthorizationToken: &testTokenName, 104 | }, 105 | }, 106 | }, 107 | token: &Token{}, 108 | expectedErr: nil, 109 | }, 110 | { 111 | name: "no authorization data", 112 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 113 | AuthorizationData: nil, 114 | }, 115 | token: &Token{}, 116 | expectedErr: errors.New("no authorization data is returned from ECR"), 117 | }, 118 | { 119 | name: "multiple authorization records", 120 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 121 | AuthorizationData: []types.AuthorizationData{ 122 | { 123 | AuthorizationToken: &testTokenName, 124 | }, 125 | { 126 | AuthorizationToken: &testTokenName, 127 | }, 128 | }, 129 | }, 130 | token: &Token{}, 131 | expectedErr: errors.New("multiple authorization records are returned for ECR"), 132 | }, 133 | { 134 | name: "authorization token is empty", 135 | mockTokenOutput: &ecr.GetAuthorizationTokenOutput{ 136 | AuthorizationData: []types.AuthorizationData{{}}, 137 | }, 138 | token: &Token{}, 139 | expectedErr: errors.New("no authorization data is returned from ECR - authorization token is empty"), 140 | }, 141 | } 142 | for _, tt := range tests { 143 | tt := tt 144 | t.Run(tt.name, func(t *testing.T) { 145 | t.Parallel() 146 | mockClient := &MockECRClient{} 147 | mockClient.On("GetAuthorizationToken", mock.Anything, mock.Anything).Return(tt.mockTokenOutput, nil) 148 | tt.token.Client = mockClient 149 | 150 | err := tt.token.Refresh(context.Background()) 151 | 152 | if tt.expectedErr != nil { 153 | assert.Equal(t, tt.expectedErr.Error(), err.Error()) 154 | } else { 155 | assert.NilError(t, err) 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /pkg/ecr/types.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package ecr 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/aws/aws-sdk-go-v2/aws" 22 | "github.com/aws/aws-sdk-go-v2/credentials/stscreds" 23 | "github.com/aws/aws-sdk-go-v2/service/sts" 24 | ) 25 | 26 | type StringableCredentials struct { 27 | aws.Credentials 28 | // Region specifies which region to connect to when using this credential 29 | Region string 30 | // Assume a role 31 | RoleArn string 32 | } 33 | 34 | func (c *StringableCredentials) GetCreds(_ context.Context) (aws.Credentials, error) { 35 | return c.Credentials, nil 36 | } 37 | 38 | func (c *StringableCredentials) ToAwsConfig() aws.Config { 39 | cfg := aws.Config{ 40 | Region: c.Region, 41 | Credentials: aws.CredentialsProviderFunc(func(_ context.Context) (aws.Credentials, error) { 42 | return c.Credentials, nil 43 | }), 44 | } 45 | if len(c.RoleArn) != 0 { 46 | // Create the credentials from AssumeRoleProvider to assume the role 47 | // referenced by the `RoleArn`. 48 | stsSvc := sts.NewFromConfig(cfg) 49 | creds := stscreds.NewAssumeRoleProvider(stsSvc, c.RoleArn) 50 | cfg.Credentials = aws.NewCredentialsCache(creds) 51 | } 52 | 53 | return cfg 54 | } 55 | 56 | func (c *StringableCredentials) Retrieve(_ context.Context) (aws.Credentials, error) { 57 | return c.Credentials, nil 58 | } 59 | 60 | func (c StringableCredentials) String() string { 61 | return fmt.Sprintf("%s/%s/%s/%s", c.Region, c.AccessKeyID, c.SecretAccessKey, c.SessionToken) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/ecr/types_test.go: -------------------------------------------------------------------------------- 1 | package ecr 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/credentials/stscreds" 9 | "github.com/aws/aws-sdk-go-v2/service/sts" 10 | "gotest.tools/assert" 11 | ) 12 | 13 | func TestStringableCredentials_GetCreds(t *testing.T) { 14 | t.Parallel() 15 | tests := []struct { 16 | name string 17 | stringableCredentials *StringableCredentials 18 | want aws.Credentials 19 | }{ 20 | { 21 | name: "empty credentials", 22 | stringableCredentials: &StringableCredentials{}, 23 | want: aws.Credentials{}, 24 | }, 25 | { 26 | name: "non-empty credentials", 27 | stringableCredentials: &StringableCredentials{ 28 | aws.Credentials{ 29 | AccessKeyID: "testAccessKeyID", 30 | }, 31 | "testRegion", 32 | "testRole", 33 | }, 34 | want: aws.Credentials{ 35 | AccessKeyID: "testAccessKeyID", 36 | }, 37 | }, 38 | } 39 | for _, tt := range tests { 40 | tt := tt 41 | t.Run(tt.name, func(t *testing.T) { 42 | t.Parallel() 43 | found, err := tt.stringableCredentials.GetCreds(context.Background()) 44 | 45 | assert.Equal(t, tt.want, found) 46 | assert.NilError(t, err) 47 | }) 48 | } 49 | } 50 | 51 | func TestStringableCredentials_ToAwsConfig(t *testing.T) { 52 | t.Parallel() 53 | tests := []struct { 54 | name string 55 | stringableCredentials *StringableCredentials 56 | want aws.Config 57 | }{ 58 | { 59 | name: "basic functionality test", 60 | stringableCredentials: &StringableCredentials{ 61 | Region: "testRegion", 62 | RoleArn: "testRole", 63 | }, 64 | want: aws.Config{ 65 | Region: "testRegion", 66 | Credentials: aws.NewCredentialsCache(stscreds.NewAssumeRoleProvider(&sts.Client{}, "testRole")), 67 | }, 68 | }, 69 | } 70 | for _, tt := range tests { 71 | tt := tt 72 | t.Run(tt.name, func(t *testing.T) { 73 | t.Parallel() 74 | found := tt.stringableCredentials.ToAwsConfig() 75 | 76 | assert.Equal(t, tt.want.Region, found.Region) 77 | wantedCred, _ := tt.want.Credentials.Retrieve(context.Background()) 78 | foundCreds, _ := found.Credentials.Retrieve(context.Background()) 79 | assert.Equal(t, wantedCred, foundCreds) 80 | }) 81 | } 82 | } 83 | 84 | func TestStringableCredentials_Retrieve(t *testing.T) { 85 | t.Parallel() 86 | tests := []struct { 87 | name string 88 | stringableCredentials *StringableCredentials 89 | want aws.Credentials 90 | }{ 91 | { 92 | name: "empty credentials", 93 | stringableCredentials: &StringableCredentials{}, 94 | want: aws.Credentials{}, 95 | }, 96 | { 97 | name: "non-empty credentials", 98 | stringableCredentials: &StringableCredentials{ 99 | aws.Credentials{ 100 | AccessKeyID: "testAccessKeyID", 101 | }, 102 | "testRegion", 103 | "testRole", 104 | }, 105 | want: aws.Credentials{ 106 | AccessKeyID: "testAccessKeyID", 107 | }, 108 | }, 109 | } 110 | for _, tt := range tests { 111 | tt := tt 112 | t.Run(tt.name, func(t *testing.T) { 113 | t.Parallel() 114 | found, err := tt.stringableCredentials.Retrieve(context.Background()) 115 | 116 | assert.Equal(t, tt.want, found) 117 | assert.NilError(t, err) 118 | }) 119 | } 120 | } 121 | 122 | func TestStringableCredentials_String(t *testing.T) { 123 | t.Parallel() 124 | tests := []struct { 125 | name string 126 | stringableCredentials *StringableCredentials 127 | want string 128 | }{ 129 | { 130 | name: "empty credentials", 131 | stringableCredentials: &StringableCredentials{}, 132 | want: "///", 133 | }, 134 | { 135 | name: "partially empty credentials", 136 | stringableCredentials: &StringableCredentials{ 137 | aws.Credentials{ 138 | AccessKeyID: "testAccessKeyID", 139 | }, 140 | "testRegion", 141 | "testRole", 142 | }, 143 | want: "testRegion/testAccessKeyID//", 144 | }, 145 | { 146 | name: "non-empty credentials", 147 | stringableCredentials: &StringableCredentials{ 148 | aws.Credentials{ 149 | AccessKeyID: "testAccessKeyID", 150 | SecretAccessKey: "testSecretAccessKey", 151 | SessionToken: "testSessionToken", 152 | }, 153 | "testRegion", 154 | "testRole", 155 | }, 156 | want: "testRegion/testAccessKeyID/testSecretAccessKey/testSessionToken", 157 | }, 158 | } 159 | for _, tt := range tests { 160 | tt := tt 161 | t.Run(tt.name, func(t *testing.T) { 162 | t.Parallel() 163 | found := tt.stringableCredentials.String() 164 | 165 | assert.Equal(t, tt.want, found) 166 | }) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /pkg/pullsecrets/docker_config.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pullsecrets 16 | 17 | import ( 18 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "time" 22 | 23 | "emperror.dev/errors" 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/types" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | 29 | "github.com/banzaicloud/imps/api/common" 30 | ) 31 | 32 | func NewDockerRegistryConfig() common.DockerRegistryConfig { 33 | return common.DockerRegistryConfig{ 34 | Auths: map[string]common.LoginCredentials{}, 35 | } 36 | } 37 | 38 | type LoginCredentialsWithDetails struct { 39 | common.LoginCredentials 40 | Expiration *time.Time 41 | URL string 42 | } 43 | 44 | type LoginCredentialProvider interface { 45 | LoginCredentials(context.Context) ([]LoginCredentialsWithDetails, error) 46 | } 47 | 48 | type Config struct { 49 | Registries map[string]LoginCredentialProvider 50 | } 51 | 52 | func NewConfig() *Config { 53 | return &Config{ 54 | Registries: map[string]LoginCredentialProvider{}, 55 | } 56 | } 57 | 58 | func NewConfigFromSecrets(ctx context.Context, c client.Client, refs []types.NamespacedName) *Config { 59 | var secret corev1.Secret 60 | config := NewConfig() 61 | 62 | for _, secretRef := range refs { 63 | secretName := fmt.Sprintf("%s.%s", secretRef.Namespace, secretRef.Name) 64 | err := c.Get(ctx, client.ObjectKey{ 65 | Namespace: secretRef.Namespace, 66 | Name: secretRef.Name, 67 | }, &secret) 68 | if err != nil { 69 | config.Registries[secretName] = NewErroredCredentialProvider(err) 70 | 71 | continue 72 | } 73 | 74 | switch secret.Type { 75 | case common.SecretTypeBasicAuth: 76 | dockerConfig, found := secret.Data[common.SecretKeyDockerConfig] 77 | if !found { 78 | config.Registries[secretName] = NewErroredCredentialProvider(errors.NewWithDetails("no docker configuration found in secret", "secret", secret.ObjectMeta)) 79 | 80 | continue 81 | } 82 | config.Registries[secretName] = config.StaticProviderFromDockerConfig(dockerConfig) 83 | case common.SecretTypeECRCredentials: 84 | config.Registries[secretName] = config.ECRProviderFromSecret(secret.Data) 85 | default: 86 | config.Registries[secretName] = NewErroredCredentialProvider( 87 | errors.NewWithDetails("unknown secret type", "type", secret.Type, "secret", secret.ObjectMeta)) 88 | } 89 | } 90 | 91 | return config 92 | } 93 | 94 | func (c *Config) StaticProviderFromDockerConfig(data []byte) LoginCredentialProvider { 95 | var dockerConfig common.DockerRegistryConfig 96 | err := json.Unmarshal(data, &dockerConfig) 97 | if err != nil { 98 | return NewErroredCredentialProvider(err) 99 | } 100 | 101 | return NewStaticLoginCredentialProvider(dockerConfig) 102 | } 103 | 104 | func getOptionalFieldFromMap(data map[string][]byte, key string, defaultVal string) string { 105 | value, found := data[key] 106 | if !found { 107 | return defaultVal 108 | } 109 | 110 | return string(value) 111 | } 112 | 113 | func getFieldFromMap(data map[string][]byte, key string) (string, error) { 114 | value, found := data[key] 115 | if !found { 116 | return "", fmt.Errorf("no such key: %s", key) 117 | } 118 | 119 | return string(value), nil 120 | } 121 | 122 | func (c *Config) ECRProviderFromSecret(data map[string][]byte) LoginCredentialProvider { 123 | accountID, err := getFieldFromMap(data, common.ECRSecretAccountID) 124 | if err != nil { 125 | return NewErroredCredentialProvider(err) 126 | } 127 | 128 | region, err := getFieldFromMap(data, common.ECRSecretRegion) 129 | if err != nil { 130 | return NewErroredCredentialProvider(err) 131 | } 132 | 133 | accKeyID, err := getFieldFromMap(data, common.ECRSecretKeyAccessKeyID) 134 | if err != nil { 135 | return NewErroredCredentialProvider(err) 136 | } 137 | 138 | secretKey, err := getFieldFromMap(data, common.ECRSecretSecretKey) 139 | if err != nil { 140 | return NewErroredCredentialProvider(err) 141 | } 142 | 143 | roleArn := getOptionalFieldFromMap(data, common.ECRRoleArn, "") 144 | 145 | return NewECRLoginCredentialsProvider(accountID, region, accKeyID, secretKey, roleArn, nil) 146 | } 147 | 148 | type ResultingDockerConfig struct { 149 | ErrorsPerSecret 150 | ConfigContents []byte 151 | Expiration *time.Time 152 | } 153 | 154 | func (c Config) ResultingDockerConfig(ctx context.Context) (*ResultingDockerConfig, error) { 155 | finalRegistryConfig := NewDockerRegistryConfig() 156 | var minExpiration *time.Time 157 | secretErrors := NewErrorsPerSecret() 158 | 159 | for secret, provider := range c.Registries { 160 | secretErrors.AddSecret(secret) 161 | credentials, err := provider.LoginCredentials(ctx) 162 | if err != nil { 163 | secretErrors.SetSecretError(secret, err) 164 | 165 | continue 166 | } 167 | 168 | for _, credential := range credentials { 169 | if credential.Expiration != nil { 170 | if minExpiration == nil || (credential.Expiration).Before(*minExpiration) { 171 | minExpiration = credential.Expiration 172 | } 173 | } 174 | finalRegistryConfig.Auths[credential.URL] = credential.LoginCredentials 175 | } 176 | } 177 | 178 | marshaledRegistryConfig, err := json.Marshal(finalRegistryConfig) 179 | if err != nil { 180 | return nil, errors.Wrap(err, "cannot encode docker configuration into a JSON") 181 | } 182 | 183 | return &ResultingDockerConfig{ 184 | ErrorsPerSecret: secretErrors, 185 | ConfigContents: marshaledRegistryConfig, 186 | Expiration: minExpiration, 187 | }, nil 188 | } 189 | 190 | func (c ResultingDockerConfig) AsSecret(secretNamespace, secretName string) *corev1.Secret { 191 | return &corev1.Secret{ 192 | ObjectMeta: metav1.ObjectMeta{ 193 | Name: secretName, 194 | Namespace: secretNamespace, 195 | }, 196 | Type: common.SecretTypeBasicAuth, 197 | StringData: map[string]string{ 198 | common.SecretKeyDockerConfig: string(c.ConfigContents), 199 | }, 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /pkg/pullsecrets/docker_config_test.go: -------------------------------------------------------------------------------- 1 | package pullsecrets 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "gotest.tools/assert" 9 | corev1 "k8s.io/api/core/v1" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | 12 | "github.com/banzaicloud/imps/api/common" 13 | ) 14 | 15 | func TestDockerConfig_NewDockerRegistryConfig(t *testing.T) { 16 | t.Parallel() 17 | tests := []struct { 18 | name string 19 | want common.DockerRegistryConfig 20 | }{ 21 | { 22 | name: "basic functionality test", 23 | want: common.DockerRegistryConfig{ 24 | Auths: make(map[string]common.LoginCredentials), 25 | }, 26 | }, 27 | } 28 | for _, tt := range tests { 29 | tt := tt 30 | t.Run(tt.name, func(t *testing.T) { 31 | t.Parallel() 32 | found := NewDockerRegistryConfig() 33 | 34 | assert.DeepEqual(t, tt.want, found) 35 | }) 36 | } 37 | } 38 | 39 | func TestDockerConfig_NewConfig(t *testing.T) { 40 | t.Parallel() 41 | tests := []struct { 42 | name string 43 | want *Config 44 | }{ 45 | { 46 | name: "basic functionality test", 47 | want: &Config{ 48 | Registries: make(map[string]LoginCredentialProvider), 49 | }, 50 | }, 51 | } 52 | for _, tt := range tests { 53 | tt := tt 54 | t.Run(tt.name, func(t *testing.T) { 55 | t.Parallel() 56 | found := NewConfig() 57 | 58 | assert.DeepEqual(t, tt.want, found) 59 | }) 60 | } 61 | } 62 | 63 | func TestDockerConfig_StaticProviderFromDockerConfig(t *testing.T) { 64 | t.Parallel() 65 | type args struct { 66 | data []byte 67 | } 68 | 69 | tests := []struct { 70 | name string 71 | args args 72 | config Config 73 | want LoginCredentialProvider 74 | }{ 75 | { 76 | name: "basic functionality test", 77 | args: args{ 78 | data: []byte("{}"), 79 | }, 80 | config: Config{}, 81 | want: StaticLoginCredentialProvider{ 82 | Credentials: []LoginCredentialsWithDetails{}, 83 | }, 84 | }, 85 | } 86 | for _, tt := range tests { 87 | tt := tt 88 | t.Run(tt.name, func(t *testing.T) { 89 | t.Parallel() 90 | found := tt.config.StaticProviderFromDockerConfig(tt.args.data) 91 | 92 | assert.DeepEqual(t, tt.want, found) 93 | }) 94 | } 95 | } 96 | 97 | func TestDockerConfig_getOptionalFieldFromMap(t *testing.T) { 98 | t.Parallel() 99 | type args struct { 100 | data map[string][]byte 101 | key string 102 | defaultVal string 103 | } 104 | 105 | tests := []struct { 106 | name string 107 | args args 108 | want string 109 | }{ 110 | { 111 | name: "value not found in data", 112 | args: args{ 113 | data: map[string][]byte{}, 114 | key: "testKey", 115 | defaultVal: "testDefault", 116 | }, 117 | want: "testDefault", 118 | }, 119 | { 120 | name: "value is found in data", 121 | args: args{ 122 | data: map[string][]byte{ 123 | "testKey": []byte("testData"), 124 | }, 125 | key: "testKey", 126 | defaultVal: "testDefault", 127 | }, 128 | want: "testData", 129 | }, 130 | } 131 | for _, tt := range tests { 132 | tt := tt 133 | t.Run(tt.name, func(t *testing.T) { 134 | t.Parallel() 135 | found := getOptionalFieldFromMap(tt.args.data, tt.args.key, tt.args.defaultVal) 136 | 137 | assert.Equal(t, tt.want, found) 138 | }) 139 | } 140 | } 141 | 142 | func TestDockerConfig_getFieldFromMap(t *testing.T) { 143 | t.Parallel() 144 | type args struct { 145 | data map[string][]byte 146 | key string 147 | } 148 | 149 | tests := []struct { 150 | name string 151 | args args 152 | want string 153 | }{ 154 | { 155 | name: "value not found in data", 156 | args: args{ 157 | data: map[string][]byte{}, 158 | key: "testKey", 159 | }, 160 | want: "", 161 | }, 162 | { 163 | name: "value is found in data", 164 | args: args{ 165 | data: map[string][]byte{ 166 | "testKey": []byte("testData"), 167 | }, 168 | key: "testKey", 169 | }, 170 | want: "testData", 171 | }, 172 | } 173 | for _, tt := range tests { 174 | tt := tt 175 | t.Run(tt.name, func(t *testing.T) { 176 | t.Parallel() 177 | found, err := getFieldFromMap(tt.args.data, tt.args.key) 178 | 179 | assert.Equal(t, tt.want, found) 180 | if err != nil { 181 | assert.ErrorContains(t, err, "no such key") 182 | } 183 | }) 184 | } 185 | } 186 | 187 | func TestDockerConfig_ECRProviderFromSecret(t *testing.T) { 188 | t.Parallel() 189 | type args struct { 190 | data map[string][]byte 191 | } 192 | 193 | tests := []struct { 194 | name string 195 | args args 196 | config Config 197 | want LoginCredentialProvider 198 | }{ 199 | { 200 | name: "basic functionality test", 201 | args: args{ 202 | data: map[string][]byte{ 203 | "accountID": []byte("testAccountID"), 204 | "region": []byte("testRegion"), 205 | "accessKeyID": []byte("testAccessKeyID"), 206 | "secretKey": []byte("testSecretKey"), 207 | "roleArn": []byte("testRole"), 208 | }, 209 | }, 210 | config: Config{}, 211 | want: ECRLoginCredentialsProvider{ 212 | Region: "testRegion", 213 | AccountID: "testAccountID", 214 | RoleArn: "testRole", 215 | Credentials: aws.Credentials{ 216 | AccessKeyID: "testAccessKeyID", 217 | SecretAccessKey: "testSecretKey", 218 | }, 219 | }, 220 | }, 221 | } 222 | for _, tt := range tests { 223 | tt := tt 224 | t.Run(tt.name, func(t *testing.T) { 225 | t.Parallel() 226 | found := tt.config.ECRProviderFromSecret(tt.args.data) 227 | 228 | assert.DeepEqual(t, tt.want, found) 229 | }) 230 | } 231 | } 232 | 233 | func TestDockerConfig_ResultingDockerConfig(t *testing.T) { 234 | t.Parallel() 235 | 236 | tests := []struct { 237 | name string 238 | config Config 239 | want *ResultingDockerConfig 240 | }{ 241 | { 242 | name: "empty config", 243 | config: Config{}, 244 | want: &ResultingDockerConfig{ 245 | ErrorsPerSecret: ErrorsPerSecret{}, 246 | ConfigContents: []uint8(`{"auths":{}}`), 247 | }, 248 | }, 249 | { 250 | name: "non-empty config", 251 | config: Config{ 252 | Registries: map[string]LoginCredentialProvider{ 253 | "testProvider": StaticLoginCredentialProvider{ 254 | Credentials: []LoginCredentialsWithDetails{ 255 | { 256 | URL: "test.url", 257 | }, 258 | }, 259 | }, 260 | }, 261 | }, 262 | want: &ResultingDockerConfig{ 263 | ErrorsPerSecret: ErrorsPerSecret{"testProvider": nil}, 264 | ConfigContents: []byte(`{"auths":{"test.url":{"username":"","password":"","auth":""}}}`), 265 | }, 266 | }, 267 | } 268 | for _, tt := range tests { 269 | tt := tt 270 | t.Run(tt.name, func(t *testing.T) { 271 | t.Parallel() 272 | found, err := tt.config.ResultingDockerConfig(context.Background()) 273 | 274 | assert.DeepEqual(t, tt.want, found) 275 | assert.NilError(t, err) 276 | }) 277 | } 278 | } 279 | 280 | func TestDockerConfig_AsSecret(t *testing.T) { 281 | t.Parallel() 282 | type args struct { 283 | secretNamespace string 284 | secretName string 285 | } 286 | 287 | tests := []struct { 288 | name string 289 | args args 290 | resultingDockerConfig ResultingDockerConfig 291 | want *corev1.Secret 292 | }{ 293 | { 294 | name: "basic functionality test", 295 | args: args{ 296 | secretName: "testSecret", 297 | secretNamespace: "testSecretNamespace", 298 | }, 299 | want: &corev1.Secret{ 300 | ObjectMeta: v1.ObjectMeta{ 301 | Name: "testSecret", 302 | Namespace: "testSecretNamespace", 303 | }, 304 | Type: common.SecretTypeBasicAuth, 305 | StringData: map[string]string{ 306 | common.SecretKeyDockerConfig: "", 307 | }, 308 | }, 309 | }, 310 | } 311 | for _, tt := range tests { 312 | tt := tt 313 | t.Run(tt.name, func(t *testing.T) { 314 | t.Parallel() 315 | found := tt.resultingDockerConfig.AsSecret(tt.args.secretNamespace, tt.args.secretName) 316 | 317 | assert.DeepEqual(t, tt.want, found) 318 | }) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /pkg/pullsecrets/error.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Cisco Systems 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pullsecrets 16 | 17 | import "emperror.dev/errors" 18 | 19 | const ( 20 | SourceSecretStatus = "Ok" 21 | ) 22 | 23 | type ErrorsPerSecret map[string]error 24 | 25 | func NewErrorsPerSecret() ErrorsPerSecret { 26 | return ErrorsPerSecret{} 27 | } 28 | 29 | func (e ErrorsPerSecret) AddSecret(name string) { 30 | e[name] = nil 31 | } 32 | 33 | func (e ErrorsPerSecret) SetSecretError(name string, err error) { 34 | e[name] = err 35 | } 36 | 37 | func (e ErrorsPerSecret) AsStatus() map[string]string { 38 | status := map[string]string{} 39 | for secret, err := range e { 40 | if err == nil { 41 | status[secret] = SourceSecretStatus 42 | } else { 43 | status[secret] = err.Error() 44 | } 45 | } 46 | 47 | return status 48 | } 49 | 50 | func (e ErrorsPerSecret) FailedSecrets() []string { 51 | invalidSecrets := []string{} 52 | 53 | for secret, err := range e { 54 | if err != nil { 55 | invalidSecrets = append(invalidSecrets, secret) 56 | } 57 | } 58 | 59 | return invalidSecrets 60 | } 61 | 62 | func (e *ErrorsPerSecret) AsError() error { 63 | invalidSecrets := e.FailedSecrets() 64 | 65 | if len(invalidSecrets) == 0 { 66 | return nil 67 | } 68 | 69 | return errors.NewWithDetails("some source secrets failed to render", "failed_secrets", invalidSecrets) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/pullsecrets/error_test.go: -------------------------------------------------------------------------------- 1 | package pullsecrets 2 | 3 | import ( 4 | "testing" 5 | 6 | "emperror.dev/errors" 7 | "gotest.tools/assert" 8 | ) 9 | 10 | func TestError_NewErrorsPerSecret(t *testing.T) { 11 | t.Parallel() 12 | tests := []struct { 13 | name string 14 | want ErrorsPerSecret 15 | }{ 16 | { 17 | name: "basic functionality test", 18 | want: ErrorsPerSecret{}, 19 | }, 20 | } 21 | for _, tt := range tests { 22 | tt := tt 23 | t.Run(tt.name, func(t *testing.T) { 24 | t.Parallel() 25 | found := NewErrorsPerSecret() 26 | 27 | assert.DeepEqual(t, tt.want, found) 28 | }) 29 | } 30 | } 31 | 32 | func TestError_AddSecret(t *testing.T) { 33 | t.Parallel() 34 | type args struct { 35 | name string 36 | } 37 | 38 | tests := []struct { 39 | name string 40 | args args 41 | errorsPerSecret ErrorsPerSecret 42 | }{ 43 | { 44 | name: "basic functionality test", 45 | args: args{ 46 | name: "testSecret", 47 | }, 48 | errorsPerSecret: ErrorsPerSecret{}, 49 | }, 50 | } 51 | for _, tt := range tests { 52 | tt := tt 53 | t.Run(tt.name, func(t *testing.T) { 54 | t.Parallel() 55 | tt.errorsPerSecret.AddSecret(tt.args.name) 56 | 57 | assert.Assert(t, tt.errorsPerSecret["testSecret"] == nil) 58 | }) 59 | } 60 | } 61 | 62 | func TestError_SetSecretError(t *testing.T) { 63 | t.Parallel() 64 | type args struct { 65 | name string 66 | err error 67 | } 68 | 69 | tests := []struct { 70 | name string 71 | args args 72 | errorsPerSecret ErrorsPerSecret 73 | expectedErrorMessage string 74 | }{ 75 | { 76 | name: "empty error message", 77 | args: args{ 78 | name: "testSecret", 79 | err: errors.New(""), 80 | }, 81 | errorsPerSecret: ErrorsPerSecret{}, 82 | expectedErrorMessage: "", 83 | }, 84 | { 85 | name: "non-empty error message", 86 | args: args{ 87 | name: "testSecret", 88 | err: errors.New("testError"), 89 | }, 90 | errorsPerSecret: ErrorsPerSecret{}, 91 | expectedErrorMessage: "testError", 92 | }, 93 | } 94 | for _, tt := range tests { 95 | tt := tt 96 | t.Run(tt.name, func(t *testing.T) { 97 | t.Parallel() 98 | tt.errorsPerSecret.SetSecretError(tt.args.name, tt.args.err) 99 | 100 | assert.Assert(t, tt.errorsPerSecret["testSecret"].Error() == tt.expectedErrorMessage) 101 | }) 102 | } 103 | } 104 | 105 | func TestError_AsStatus(t *testing.T) { 106 | t.Parallel() 107 | testErrorPerSecret := ErrorsPerSecret{} 108 | testErrorPerSecret.SetSecretError("testSecret", errors.New("testError")) 109 | testErrorPerSecret.AddSecret("testSecret2") 110 | 111 | tests := []struct { 112 | name string 113 | errorsPerSecret ErrorsPerSecret 114 | wanted map[string]string 115 | }{ 116 | { 117 | name: "basic functionality test", 118 | errorsPerSecret: testErrorPerSecret, 119 | wanted: map[string]string{ 120 | "testSecret": "testError", 121 | "testSecret2": "Ok", 122 | }, 123 | }, 124 | } 125 | for _, tt := range tests { 126 | tt := tt 127 | t.Run(tt.name, func(t *testing.T) { 128 | t.Parallel() 129 | found := tt.errorsPerSecret.AsStatus() 130 | 131 | assert.DeepEqual(t, tt.wanted, found) 132 | }) 133 | } 134 | } 135 | 136 | func TestError_FailedSecrets(t *testing.T) { 137 | t.Parallel() 138 | testErrorPerSecret := ErrorsPerSecret{} 139 | testErrorPerSecret.SetSecretError("testSecret", errors.New("testError")) 140 | testErrorPerSecret.AddSecret("testSecret2") 141 | 142 | tests := []struct { 143 | name string 144 | errorsPerSecret ErrorsPerSecret 145 | wanted []string 146 | }{ 147 | { 148 | name: "basic functionality test", 149 | errorsPerSecret: testErrorPerSecret, 150 | wanted: []string{"testSecret"}, 151 | }, 152 | } 153 | for _, tt := range tests { 154 | tt := tt 155 | t.Run(tt.name, func(t *testing.T) { 156 | t.Parallel() 157 | found := tt.errorsPerSecret.FailedSecrets() 158 | 159 | assert.DeepEqual(t, tt.wanted, found) 160 | }) 161 | } 162 | } 163 | 164 | func TestError_AsError(t *testing.T) { 165 | t.Parallel() 166 | testErrorPerSecret := ErrorsPerSecret{} 167 | testErrorPerSecret.SetSecretError("testSecret", errors.New("testError")) 168 | testErrorPerSecret.AddSecret("testSecret2") 169 | 170 | tests := []struct { 171 | name string 172 | errorsPerSecret ErrorsPerSecret 173 | wanted error 174 | }{ 175 | { 176 | name: "no invalid secrets", 177 | errorsPerSecret: ErrorsPerSecret{}, 178 | wanted: nil, 179 | }, 180 | { 181 | name: "one invalid secret", 182 | errorsPerSecret: testErrorPerSecret, 183 | wanted: errors.NewWithDetails("some source secrets failed to render", "failed_secrets", "testSecret"), 184 | }, 185 | } 186 | for _, tt := range tests { 187 | tt := tt 188 | t.Run(tt.name, func(t *testing.T) { 189 | t.Parallel() 190 | found := tt.errorsPerSecret.AsError() 191 | 192 | if len(tt.errorsPerSecret.FailedSecrets()) > 0 { 193 | assert.Equal(t, tt.wanted.Error(), found.Error()) 194 | } else { 195 | assert.Equal(t, tt.wanted, found) 196 | } 197 | }) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /pkg/pullsecrets/provider_ecr.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pullsecrets 16 | 17 | import ( 18 | "context" 19 | "encoding/base64" 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/aws/aws-sdk-go-v2/aws" 24 | 25 | "github.com/banzaicloud/imps/api/common" 26 | impsEcr "github.com/banzaicloud/imps/pkg/ecr" 27 | ) 28 | 29 | type ECRLoginCredentialsProvider struct { 30 | Credentials aws.Credentials 31 | Region string 32 | AccountID string 33 | RoleArn string 34 | Client impsEcr.ClientInterface 35 | } 36 | 37 | func NewECRLoginCredentialsProvider(accountID, region, keyID, secretAccessKey string, roleArn string, client impsEcr.ClientInterface) ECRLoginCredentialsProvider { 38 | return ECRLoginCredentialsProvider{ 39 | Credentials: aws.Credentials{ 40 | AccessKeyID: keyID, 41 | SecretAccessKey: secretAccessKey, 42 | }, 43 | AccountID: accountID, 44 | Region: region, 45 | RoleArn: roleArn, 46 | Client: client, 47 | } 48 | } 49 | 50 | func (p ECRLoginCredentialsProvider) GetURL() string { 51 | return fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", p.AccountID, p.Region) 52 | } 53 | 54 | func (p ECRLoginCredentialsProvider) LoginCredentials(ctx context.Context) ([]LoginCredentialsWithDetails, error) { 55 | token, err := impsEcr.GetAuthorizationToken(ctx, p.Region, p.Credentials, p.RoleArn, p.Client) 56 | if err != nil { 57 | return nil, err 58 | } 59 | decodedAuth, err := base64.StdEncoding.DecodeString(*token.AuthorizationToken) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | splitAuth := strings.SplitN(string(decodedAuth), ":", 2) 65 | 66 | return []LoginCredentialsWithDetails{ 67 | { 68 | LoginCredentials: common.LoginCredentials{ 69 | Username: splitAuth[0], 70 | Password: splitAuth[1], 71 | Auth: *token.AuthorizationToken, 72 | }, 73 | URL: p.GetURL(), 74 | Expiration: token.ExpiresAt, 75 | }, 76 | }, nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/pullsecrets/provider_ecr_test.go: -------------------------------------------------------------------------------- 1 | package pullsecrets 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "testing" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/ecr" 10 | "github.com/aws/aws-sdk-go-v2/service/ecr/types" 11 | "github.com/stretchr/testify/mock" 12 | "gotest.tools/assert" 13 | "logur.dev/logur" 14 | 15 | "github.com/banzaicloud/imps/api/common" 16 | impsEcr "github.com/banzaicloud/imps/pkg/ecr" 17 | ) 18 | 19 | func TestECRLoginCredentialsProvider_NewECRLoginCredentialsProvider(t *testing.T) { 20 | t.Parallel() 21 | type args struct { 22 | accountID string 23 | region string 24 | keyID string 25 | secretAccessKey string 26 | roleArn string 27 | } 28 | 29 | tests := []struct { 30 | name string 31 | args args 32 | wanted ECRLoginCredentialsProvider 33 | }{ 34 | { 35 | name: "basic functionality test", 36 | args: args{ 37 | accountID: "testAccountID", 38 | region: "testRegion", 39 | keyID: "testKeyID", 40 | secretAccessKey: "testSecretAccessKey", 41 | roleArn: "testRole", 42 | }, 43 | wanted: ECRLoginCredentialsProvider{ 44 | Region: "testRegion", 45 | AccountID: "testAccountID", 46 | RoleArn: "testRole", 47 | Credentials: aws.Credentials{ 48 | SecretAccessKey: "testSecretAccessKey", 49 | AccessKeyID: "testKeyID", 50 | }, 51 | }, 52 | }, 53 | } 54 | for _, tt := range tests { 55 | tt := tt 56 | t.Run(tt.name, func(t *testing.T) { 57 | t.Parallel() 58 | found := NewECRLoginCredentialsProvider(tt.args.accountID, tt.args.region, tt.args.keyID, tt.args.secretAccessKey, tt.args.roleArn, nil) 59 | 60 | assert.DeepEqual(t, tt.wanted, found) 61 | }) 62 | } 63 | } 64 | 65 | func TestECRLoginCredentialsProvider_GetURL(t *testing.T) { 66 | t.Parallel() 67 | tests := []struct { 68 | name string 69 | ecrLoginCredentialsProvider ECRLoginCredentialsProvider 70 | wanted string 71 | }{ 72 | { 73 | name: "empty provider", 74 | ecrLoginCredentialsProvider: ECRLoginCredentialsProvider{}, 75 | wanted: ".dkr.ecr..amazonaws.com", 76 | }, 77 | { 78 | name: "non-empty provider", 79 | ecrLoginCredentialsProvider: ECRLoginCredentialsProvider{ 80 | AccountID: "testAccountID", 81 | Region: "testRegion", 82 | }, 83 | wanted: "testAccountID.dkr.ecr.testRegion.amazonaws.com", 84 | }, 85 | } 86 | for _, tt := range tests { 87 | tt := tt 88 | t.Run(tt.name, func(t *testing.T) { 89 | t.Parallel() 90 | found := tt.ecrLoginCredentialsProvider.GetURL() 91 | 92 | assert.DeepEqual(t, tt.wanted, found) 93 | }) 94 | } 95 | } 96 | 97 | type MockECRClient struct { 98 | mock.Mock 99 | } 100 | 101 | func (m *MockECRClient) GetAuthorizationToken(ctx context.Context, input *ecr.GetAuthorizationTokenInput, _ ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) { 102 | args := m.Called(ctx, input) 103 | // nolint:forcetypeassert 104 | return args.Get(0).(*ecr.GetAuthorizationTokenOutput), args.Error(1) 105 | } 106 | 107 | func TestECRLoginCredentialsProvider_LoginCredentials(t *testing.T) { 108 | t.Parallel() 109 | mockClient := &MockECRClient{} 110 | testTokenName := base64.StdEncoding.EncodeToString([]byte("testUser:testPass")) 111 | mockTokenOutput := &ecr.GetAuthorizationTokenOutput{ 112 | AuthorizationData: []types.AuthorizationData{ 113 | { 114 | AuthorizationToken: &testTokenName, 115 | }, 116 | }, 117 | } 118 | mockClient.On("GetAuthorizationToken", mock.Anything, mock.Anything).Return(mockTokenOutput, nil) 119 | 120 | tests := []struct { 121 | name string 122 | ecrLoginCredentialsProvider ECRLoginCredentialsProvider 123 | wanted []LoginCredentialsWithDetails 124 | }{ 125 | { 126 | name: "basic functionality test", 127 | ecrLoginCredentialsProvider: ECRLoginCredentialsProvider{ 128 | Region: "testRegion", 129 | AccountID: "testAccountID", 130 | RoleArn: "testRole", 131 | Credentials: aws.Credentials{ 132 | SecretAccessKey: "testSecretAccessKey", 133 | AccessKeyID: "testKeyID", 134 | }, 135 | Client: mockClient, 136 | }, 137 | wanted: []LoginCredentialsWithDetails{ 138 | { 139 | LoginCredentials: common.LoginCredentials{ 140 | Username: "testUser", 141 | Password: "testPass", 142 | Auth: "dGVzdFVzZXI6dGVzdFBhc3M=", 143 | }, 144 | URL: "testAccountID.dkr.ecr.testRegion.amazonaws.com", 145 | }, 146 | }, 147 | }, 148 | } 149 | for _, tt := range tests { 150 | tt := tt 151 | t.Run(tt.name, func(t *testing.T) { 152 | t.Parallel() 153 | impsEcr.Initialize(&logur.TestLogger{}) 154 | found, err := tt.ecrLoginCredentialsProvider.LoginCredentials(context.Background()) 155 | 156 | assert.DeepEqual(t, tt.wanted, found) 157 | assert.NilError(t, err) 158 | }) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /pkg/pullsecrets/provider_error.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Cisco Systems 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pullsecrets 16 | 17 | import "context" 18 | 19 | /* 20 | ErroredCredentialProvider can be used to store a setup error into the config object 21 | 22 | so that at least the providers that are working correctly gets reconciled. 23 | */ 24 | type ErroredCredentialProvider struct { 25 | Error error 26 | } 27 | 28 | func NewErroredCredentialProvider(err error) ErroredCredentialProvider { 29 | return ErroredCredentialProvider{ 30 | Error: err, 31 | } 32 | } 33 | 34 | func (p ErroredCredentialProvider) LoginCredentials(_ context.Context) ([]LoginCredentialsWithDetails, error) { 35 | return nil, p.Error 36 | } 37 | -------------------------------------------------------------------------------- /pkg/pullsecrets/provider_error_test.go: -------------------------------------------------------------------------------- 1 | package pullsecrets 2 | 3 | import ( 4 | "testing" 5 | 6 | "emperror.dev/errors" 7 | "gotest.tools/assert" 8 | ) 9 | 10 | func TestErroredCredentialProvider_NewErroredCredentialProvider(t *testing.T) { 11 | t.Parallel() 12 | type args struct { 13 | err error 14 | } 15 | 16 | tests := []struct { 17 | name string 18 | args args 19 | wanted ErroredCredentialProvider 20 | }{ 21 | { 22 | name: "empty error", 23 | args: args{ 24 | err: nil, 25 | }, 26 | wanted: ErroredCredentialProvider{ 27 | Error: nil, 28 | }, 29 | }, 30 | { 31 | name: "non-empty error", 32 | args: args{ 33 | err: errors.New("testError"), 34 | }, 35 | wanted: ErroredCredentialProvider{ 36 | Error: errors.New("testError"), 37 | }, 38 | }, 39 | } 40 | for _, tt := range tests { 41 | tt := tt 42 | t.Run(tt.name, func(t *testing.T) { 43 | t.Parallel() 44 | found := NewErroredCredentialProvider(tt.args.err) 45 | 46 | if tt.wanted.Error != nil { 47 | assert.Equal(t, tt.wanted.Error.Error(), found.Error.Error()) 48 | } else { 49 | assert.DeepEqual(t, tt.wanted, found) 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/pullsecrets/provider_static.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Banzai Cloud 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pullsecrets 16 | 17 | import ( 18 | "context" 19 | 20 | "github.com/banzaicloud/imps/api/common" 21 | ) 22 | 23 | type StaticLoginCredentialProvider struct { 24 | Credentials []LoginCredentialsWithDetails 25 | } 26 | 27 | func NewStaticLoginCredentialProvider(parsedDockerConfig common.DockerRegistryConfig) StaticLoginCredentialProvider { 28 | p := StaticLoginCredentialProvider{ 29 | Credentials: []LoginCredentialsWithDetails{}, 30 | } 31 | for url, config := range parsedDockerConfig.Auths { 32 | p.Credentials = append(p.Credentials, LoginCredentialsWithDetails{ 33 | LoginCredentials: config, 34 | URL: url, 35 | Expiration: nil, 36 | }) 37 | } 38 | 39 | return p 40 | } 41 | 42 | func (p StaticLoginCredentialProvider) LoginCredentials(_ context.Context) ([]LoginCredentialsWithDetails, error) { 43 | return p.Credentials, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/pullsecrets/provider_static_test.go: -------------------------------------------------------------------------------- 1 | package pullsecrets 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "gotest.tools/assert" 8 | 9 | "github.com/banzaicloud/imps/api/common" 10 | ) 11 | 12 | func TestStaticLoginCredentialProvider_NewStaticLoginCredentialProvider(t *testing.T) { 13 | t.Parallel() 14 | type args struct { 15 | parsedDockerConfig common.DockerRegistryConfig 16 | } 17 | 18 | tests := []struct { 19 | name string 20 | args args 21 | wanted StaticLoginCredentialProvider 22 | }{ 23 | { 24 | name: "basic functionality test", 25 | args: args{ 26 | parsedDockerConfig: common.DockerRegistryConfig{ 27 | Auths: map[string]common.LoginCredentials{ 28 | "testCreds": { 29 | Username: "testUser", 30 | }, 31 | }, 32 | }, 33 | }, 34 | wanted: StaticLoginCredentialProvider{ 35 | []LoginCredentialsWithDetails{ 36 | { 37 | LoginCredentials: common.LoginCredentials{ 38 | Username: "testUser", 39 | }, 40 | URL: "testCreds", 41 | }, 42 | }, 43 | }, 44 | }, 45 | } 46 | for _, tt := range tests { 47 | tt := tt 48 | t.Run(tt.name, func(t *testing.T) { 49 | t.Parallel() 50 | found := NewStaticLoginCredentialProvider(tt.args.parsedDockerConfig) 51 | 52 | assert.DeepEqual(t, tt.wanted, found) 53 | }) 54 | } 55 | } 56 | 57 | func TestStaticLoginCredentialProvider_LoginCredentials(t *testing.T) { 58 | t.Parallel() 59 | tests := []struct { 60 | name string 61 | staticLoginCredentialProvider StaticLoginCredentialProvider 62 | wanted []LoginCredentialsWithDetails 63 | }{ 64 | { 65 | name: "basic functionality test", 66 | staticLoginCredentialProvider: StaticLoginCredentialProvider{ 67 | []LoginCredentialsWithDetails{ 68 | { 69 | LoginCredentials: common.LoginCredentials{ 70 | Username: "testUser", 71 | }, 72 | URL: "testCreds", 73 | }, 74 | }, 75 | }, 76 | wanted: []LoginCredentialsWithDetails{ 77 | { 78 | LoginCredentials: common.LoginCredentials{ 79 | Username: "testUser", 80 | }, 81 | URL: "testCreds", 82 | }, 83 | }, 84 | }, 85 | } 86 | for _, tt := range tests { 87 | tt := tt 88 | t.Run(tt.name, func(t *testing.T) { 89 | t.Parallel() 90 | found, err := tt.staticLoginCredentialProvider.LoginCredentials(context.Background()) 91 | 92 | assert.DeepEqual(t, tt.wanted, found) 93 | assert.NilError(t, err) 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/download-deps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | controller_gen_version=0.9.2 6 | 7 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" 8 | binpath=${script_dir}/../bin 9 | 10 | mkdir -p "$binpath" 11 | 12 | function ensure-binary-version() { 13 | local bin_name=$1 14 | local bin_version=$2 15 | local download_location=$3 16 | 17 | local target_name=${bin_name}-proper-${bin_version} 18 | local link_path=${binpath}/${bin_name} 19 | 20 | if [ ! -L "${link_path}" ]; then 21 | rm -f "${link_path}" 22 | fi 23 | 24 | if [ ! -e "${binpath}/${target_name}" ]; then 25 | 26 | BUILD_DIR=$(mktemp -d) 27 | pushd "${BUILD_DIR}" 28 | GOBIN=${BUILD_DIR} go install "${download_location}" 29 | mv "${bin_name}" "${binpath}/${target_name}" 30 | popd 31 | rm -rf "${BUILD_DIR}" 32 | fi 33 | 34 | ln -sf "${target_name}" "${link_path}" 35 | } 36 | 37 | ensure-binary-version controller-gen ${controller_gen_version} "sigs.k8s.io/controller-tools/cmd/controller-gen@v${controller_gen_version}" 38 | -------------------------------------------------------------------------------- /scripts/install_envtest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | [ -z "${1:-}" ] && { echo "Usage: $0 "; exit 1; } 6 | 7 | version=$1 8 | 9 | target_dir_name=envtest-${version} 10 | link_path=bin/envtest 11 | 12 | [ -L ${link_path} ] && rm -r ${link_path} 13 | 14 | mkdir -p bin 15 | ln -s "${target_dir_name}" ${link_path} 16 | 17 | if [ ! -e bin/"${target_dir_name}" ]; then 18 | os=$(go env GOOS) 19 | arch=$(go env GOARCH) 20 | 21 | # Temporary fix for Apple M1 until envtest is released for darwin-arm64 arch 22 | if [ "$os" == "darwin" ] && [ "$arch" == "arm64" ]; then 23 | arch="amd64" 24 | fi 25 | curl -sSL "https://go.kubebuilder.io/test-tools/$version/$os/$arch" | tar -xz -C /tmp/ 26 | mv "/tmp/kubebuilder" bin/"${target_dir_name}" 27 | fi 28 | -------------------------------------------------------------------------------- /scripts/install_kustomize.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright (c) 2019 Banzai Cloud Zrt. All Rights Reserved. 4 | 5 | set -euo pipefail 6 | 7 | version=3.6.1 8 | opsys=$(uname -s | awk '{print tolower($0)}') 9 | versioned_path="bin/kustomize-${version}" 10 | 11 | if [ ! -x $versioned_path ]; then 12 | # download the release 13 | curl -O -L "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v${version}/kustomize_v${version}_${opsys}_amd64.tar.gz" 14 | 15 | tar xzf "./kustomize_v${version}_${opsys}_amd64.tar.gz" 16 | rm "./kustomize_v${version}_${opsys}_amd64.tar.gz" 17 | 18 | # move to bin 19 | mkdir -p bin 20 | mv kustomize bin/kustomize-${version} 21 | chmod u+x bin/kustomize-${version} 22 | fi 23 | 24 | ln -sf kustomize-${version} bin/kustomize 25 | --------------------------------------------------------------------------------