diff --git a/.github/dependatbot.yml b/.github/dependatbot.yml new file mode 100644 index 0000000..2bbfda4 --- /dev/null +++ b/.github/dependatbot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..79771fe --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,75 @@ +name: Build image + +on: + workflow_call: + inputs: + image-name: + required: true + type: string + build-platform: + required: true + type: string + secrets: + GHCR_USERNAME: + required: true + GHCR_TOKEN: + required: true + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Set up QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 + + - name: Log in to the Container registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ secrets.GHCR_USERNAME }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: ${{ inputs.image-name }} + tags: | + type=schedule,pattern={{date 'YYYYMMDD-HHmmss' tz='Europe/Paris'}} + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push + uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + with: + context: . + push: true + provenance: false + platforms: ${{ inputs.build-platform }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0 + with: + image-ref: ${{ inputs.image-name }}:${{ steps.meta.outputs.version }} + format: 'table' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dc5da0c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,175 @@ +name: CI + +on: + push: + branches: + - main + pull_request: {} + +env: + GOLANGCI_VERSION: 'v1.60.1' + KUBERNETES_VERSION: '1.29.x' + +permissions: + contents: read + +jobs: + detect-noop: + runs-on: ubuntu-latest + outputs: + noop: ${{ steps.noop.outputs.should_skip }} + + permissions: + actions: write + contents: read + + steps: + - name: Detect No-op Changes + id: noop + uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + paths_ignore: '["**.md", "**.png", "**.jpg"]' + do_not_skip: '["workflow_dispatch", "schedule", "push"]' + concurrent_skipping: false + + lint: + runs-on: ubuntu-latest + needs: detect-noop + if: needs.detect-noop.outputs.noop != 'true' && github.ref != 'refs/heads/main' + + permissions: + contents: read + pull-requests: read + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + id: setup-go + with: + go-version-file: "go.mod" + + - name: Lint + uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0 + with: + version: ${{ env.GOLANGCI_VERSION }} + skip-cache: true + skip-save-cache: true + + check-diff: + runs-on: ubuntu-latest + needs: detect-noop + if: needs.detect-noop.outputs.noop != 'true' && github.ref != 'refs/heads/main' + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + id: setup-go + with: + go-version-file: "go.mod" + + - name: Download Go modules + if: ${{ steps.setup-go.outputs.cache-hit != 'true' }} + run: go mod download + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Check Diff + run: | + make check-diff + + unit-tests: + runs-on: ubuntu-latest + needs: detect-noop + if: needs.detect-noop.outputs.noop != 'true' + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Fetch History + run: git fetch --prune --unshallow + + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + id: setup-go + with: + go-version-file: "go.mod" + + - name: Download Go modules + if: ${{ steps.setup-go.outputs.cache-hit != 'true' }} + run: go mod download + + - name: Cache envtest binaries + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 + with: + path: bin/k8s + key: ${{ runner.os }}-envtest-${{env.KUBERNETES_VERSION}} + + - name: Run Unit Tests + run: | + make test + + sonarqube: + name: SonarQube Trigger + runs-on: ubuntu-latest + needs: detect-noop + if: needs.detect-noop.outputs.noop != 'true' + continue-on-error: true + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + # Disabling shallow clone is recommended for improving relevancy of reporting + fetch-depth: 0 + + - name: SonarQube Scan + uses: sonarsource/sonarqube-scan-action@aecaf43ae57e412bd97d70ef9ce6076e672fe0a9 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST }} + with: + args: > + -Dsonar.projectKey=${{ secrets.SONAR_PROJECT_KEY }} + + repo-slug: + runs-on: ubuntu-latest + outputs: + repo_slug: ${{ steps.repo_slug.outputs.result }} + if: needs.detect-noop.outputs.noop != 'true' + + steps: + - name: Sanitize repo slug + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: repo_slug + with: + result-encoding: string + script: return 'ghcr.io/${{ github.repository }}'.toLowerCase() + + publish-artifacts: + needs: + - detect-noop + - repo-slug + if: needs.detect-noop.outputs.noop != 'true' + uses: ./.github/workflows/build.yml + + permissions: + contents: read + packages: write + + with: + image-name: ${{ needs.repo-slug.outputs.repo_slug }} + build-platform: "linux/amd64,linux/arm64,linux/s390x,linux/ppc64le" + secrets: + GHCR_USERNAME: ${{ github.actor }} + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e23a66d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,113 @@ +name: Create Release + +on: + workflow_dispatch: + inputs: + version: + description: 'version to release, e.g. v1.5.13' + required: true + default: 'v0.1.0' + source_ref: + description: 'source ref to publish from. E.g.: main or release-x.y' + required: true + default: 'main' + +jobs: + release: + name: Create Release + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + ref: ${{ github.event.inputs.source_ref }} + + - name: Sanitize repo slug + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: repo_slug + with: + result-encoding: string + script: return 'ghcr.io/${{ github.repository }}'.toLowerCase() + + - name: Create Release + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 + with: + tag_name: ${{ github.event.inputs.version }} + target_commitish: ${{ github.event.inputs.source_ref }} + generate_release_notes: true + body: | + Image: `${{ steps.repo_slug.outputs.result }}:${{ github.event.inputs.version }}` + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + repo-slug: + runs-on: ubuntu-latest + outputs: + repo_slug: ${{ steps.repo_slug.outputs.result }} + steps: + - name: Sanitize repo slug + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + id: repo_slug + with: + result-encoding: string + script: return 'ghcr.io/${{ github.repository }}'.toLowerCase() + + promote: + name: Promote Container Image + runs-on: ubuntu-latest + needs: [release, repo-slug] + + permissions: + contents: write + packages: write + + env: + SOURCE_TAG: ${{ github.event.inputs.source_ref }} + RELEASE_TAG: ${{ github.event.inputs.version }} + IMAGE_NAME: ${{ needs.repo-slug.outputs.repo_slug }} + + steps: + - name: Checkout + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + id: setup-go + with: + go-version-file: "go.mod" + + - name: Download Go modules + if: ${{ steps.setup-go.outputs.cache-hit != 'true' }} + run: go mod download + + - name: Login to Registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Promote Container Image + run: make docker-promote + + - name: Build release manifests + run: | + make build-installer + + - name: Update Release + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 + with: + tag_name: ${{ github.event.inputs.version }} + files: | + deploy/bundle.yaml + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.golangci.yml b/.golangci.yml index ca69a11..70009d3 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,6 +16,11 @@ issues: linters: - dupl - lll + exclude-dirs: + - .git + - .github + - test + linters: disable-all: true enable: diff --git a/Dockerfile b/Dockerfile index 3110e05..4faac5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.23 AS builder +FROM --platform=$BUILDPLATFORM golang:1.23 AS builder ARG TARGETOS ARG TARGETARCH @@ -25,7 +25,7 @@ RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o ma # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot +FROM --platform=$TARGETPLATFORM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 diff --git a/Makefile b/Makefile index 44a310b..4c43d28 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,30 @@ CONTAINER_TOOL ?= docker SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec +# ==================================================================================== +# Colors + +BLUE := $(shell printf "\033[34m") +YELLOW := $(shell printf "\033[33m") +RED := $(shell printf "\033[31m") +GREEN := $(shell printf "\033[32m") +CNone := $(shell printf "\033[0m") + +# ==================================================================================== +# Logger + +TIME_LONG = `date +%Y-%m-%d' '%H:%M:%S` +TIME_SHORT = `date +%H:%M:%S` +TIME = $(TIME_SHORT) + +INFO = echo ${TIME} ${BLUE}[ .. ]${CNone} +WARN = echo ${TIME} ${YELLOW}[WARN]${CNone} +ERR = echo ${TIME} ${RED}[FAIL]${CNone} +OK = echo ${TIME} ${GREEN}[ OK ]${CNone} +FAIL = (echo ${TIME} ${RED}[FAIL]${CNone} && false) + +# ==================================================================================== + .PHONY: all all: build @@ -41,6 +65,17 @@ all: build help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +##@ Conformance +.PHONY: reviewable +reviewable: generate manifests lint ## Ensure a PR is ready for review. + @go mod tidy + +.PHONY: check-diff +check-diff: reviewable ## Ensure branch is clean. + @$(INFO) checking that branch is clean + @test -z "$$(git status --porcelain)" || (echo "$$(git status --porcelain)" && $(FAIL)) + @$(OK) branch is clean + ##@ Development .PHONY: manifests @@ -114,11 +149,21 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform - $(CONTAINER_TOOL) buildx rm project-v3-builder rm Dockerfile.cross +.PHONY: docker-promote +docker-promote: ## Promote the docker image to the registry + docker manifest inspect --verbose $(IMAGE_NAME):$(SOURCE_TAG) > .tagmanifest + for digest in $$(jq -r 'if type=="array" then .[].Descriptor.digest else .Descriptor.digest end' < .tagmanifest); do \ + docker pull $(IMAGE_NAME)@$$digest; \ + done + docker manifest create $(IMAGE_NAME):$(RELEASE_TAG) \ + $$(jq -j '"--amend $(IMAGE_NAME)@" + if type=="array" then .[].Descriptor.digest else .Descriptor.digest end + " "' < .tagmanifest) + docker manifest push $(IMAGE_NAME):$(RELEASE_TAG) + .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. - mkdir -p dist - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default > dist/install.yaml + mkdir -p deploy + cd config/manager && $(KUSTOMIZE) edit set image controller=$(IMAGE_NAME):$(RELEASE_TAG) + $(KUSTOMIZE) build config/default > deploy/bundle.yaml ##@ Deployment diff --git a/changelog.json b/changelog.json new file mode 100644 index 0000000..99d6c69 --- /dev/null +++ b/changelog.json @@ -0,0 +1,18 @@ +{ + "categories": [], + "ignore_labels": [], + "sort": "ASC", + "template": "## Changes\n\n${{UNCATEGORIZED}}", + "pr_template": "- ${{TITLE}}", + "empty_template": "- no changes", + "label_extractor": [], + "transformers": [], + "max_tags_to_fetch": 200, + "max_pull_requests": 200, + "max_back_track_time_days": 365, + "exclude_merge_branches": [], + "tag_resolver": { + "method": "semver" + }, + "base_branches": [] +} diff --git a/internal/controller/rrset_controller.go b/internal/controller/rrset_controller.go index 9045758..356663a 100644 --- a/internal/controller/rrset_controller.go +++ b/internal/controller/rrset_controller.go @@ -107,6 +107,7 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl log.Error(err, "Failed to remove finalizer") return ctrl.Result{}, err } + //nolint:ineffassign lastUpdateTime = &metav1.Time{Time: time.Now().UTC()} } diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 270e3be..cd04f25 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -360,6 +360,7 @@ func getMockedKind(zoneName string) (result string) { return } +//nolint:unparam func getMockedRecordsForType(rrsetName, rrsetType string) (result []string) { rrset, _ := readFromRecordsMap(makeCanonical(rrsetName)) if string(*rrset.Type) == rrsetType { @@ -371,6 +372,7 @@ func getMockedRecordsForType(rrsetName, rrsetType string) (result []string) { return } +//nolint:unparam func getMockedTTL(rrsetName, rrsetType string) (result uint32) { rrset, _ := readFromRecordsMap(makeCanonical(rrsetName)) if string(*rrset.Type) == rrsetType { @@ -378,6 +380,8 @@ func getMockedTTL(rrsetName, rrsetType string) (result uint32) { } return } + +//nolint:unparam func getMockedComment(rrsetName, rrsetType string) (result string) { rrset, _ := readFromRecordsMap(makeCanonical(rrsetName)) if string(*rrset.Type) == rrsetType {