From 162a5dd3469328685f4c05c4d30c4ad98df027cf Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Wed, 29 May 2024 19:22:05 +0300 Subject: [PATCH] Add release workflow Signed-off-by: Stefan Prodan --- .dockerignore | 3 - .github/workflows/release.yml | 119 ++++++++++++++++++ Dockerfile | 28 ++--- Makefile | 43 +++---- .../controller/fluxinstance_controller.go | 2 + test/e2e/e2e_test.go | 16 ++- test/e2e/instance_test.go | 18 ++- test/{utils => e2e}/utils.go | 2 +- 8 files changed, 167 insertions(+), 64 deletions(-) delete mode 100644 .dockerignore create mode 100644 .github/workflows/release.yml rename test/{utils => e2e}/utils.go (99%) diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a3aab7a..0000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file -# Ignore build and test binaries. -bin/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..36b1c34 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,119 @@ +name: release +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'image tag prefix' + default: 'rc' + required: true + +permissions: + contents: read + +env: + CONTROLLER: ${{ github.event.repository.name }} + +jobs: + release: + outputs: + image_url: ${{ steps.slsa.outputs.image_url }} + image_digest: ${{ steps.slsa.outputs.image_digest }} + runs-on: ubuntu-latest + permissions: + contents: write # for creating the GitHub release. + id-token: write # for creating OIDC tokens for signing. + packages: write # for pushing and signing container images. + steps: + - name: Checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Kustomize + uses: fluxcd/pkg/actions/kustomize@main + - name: Prepare + id: prep + run: | + VERSION="${{ github.event.inputs.tag }}-${GITHUB_SHA::8}" + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION=${GITHUB_REF/refs\/tags\//} + fi + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + - name: Setup QEMU + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + - name: Setup Docker Buildx + id: buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Generate images meta + id: meta + uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1 + with: + images: | + ghcr.io/controlplaneio-fluxcd/${{ env.CONTROLLER }} + tags: | + type=raw,value=${{ steps.prep.outputs.VERSION }} + - name: Publish images + id: build-push + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 # v5.3.0 + with: + sbom: true + provenance: true + push: true + builder: ${{ steps.buildx.outputs.name }} + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + - uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 + - name: Sign images + env: + COSIGN_EXPERIMENTAL: 1 + run: | + cosign sign --yes ghcr.io/controlplaneio-fluxcd/${{ env.CONTROLLER }}@${{ steps.build-push.outputs.digest }} + - name: Create release + if: startsWith(github.ref, 'refs/tags/v') + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ github.ref_name }} --generate-notes --verify-tag + - name: Upload release artifacts + if: startsWith(github.ref, 'refs/tags/v') + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p config/release + kustomize build ./config/default > ./config/release/install.yaml + gh release upload ${{ github.ref_name }} ./config/release/install.yaml + - name: Generate SLSA metadata + id: slsa + run: | + image_url=fluxcd/${{ env.CONTROLLER }}:${{ steps.prep.outputs.version }} + echo "image_url=$image_url" >> $GITHUB_OUTPUT + + image_digest=${{ steps.build-push.outputs.digest }} + echo "image_digest=$image_digest" >> $GITHUB_OUTPUT + + ghcr-provenance: + needs: [release] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OIDC tokens for signing. + packages: write # for uploading attestations. + if: startsWith(github.ref, 'refs/tags/v') + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + with: + image: ghcr.io/${{ needs.release.outputs.image_url }} + digest: ${{ needs.release.outputs.image_digest }} + registry-username: ${{ github.actor }} + secrets: + registry-password: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 8d91a7b..36ea8c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,32 @@ -# Build the manager binary -FROM golang:1.22 AS builder +# Build the operator binary using the Docker's Debian image. +FROM --platform=${BUILDPLATFORM} golang:1.22 AS builder ARG TARGETOS ARG TARGETARCH - WORKDIR /workspace -# Copy the Go Modules manifests + +# Copy the Go Modules manifests. COPY go.mod go.mod COPY go.sum go.sum -# cache deps before building and copying source so that we don't need to re-download as much -# and so that source changes don't invalidate our downloaded layer + +# Cache the Go Modules RUN go mod download -# Copy the go source +# Copy the Go sources. COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/ internal/ -# Build -# the GOARCH has not a default value to allow the binary be built according to the host where the command -# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO -# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, -# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. +# Build the operator binary. RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o flux-operator cmd/main.go -# Use distroless as minimal base image to package the manager binary -# Refer to https://github.com/GoogleContainerTools/distroless for more details +# Run the operator binary using Google's Distroless image. FROM gcr.io/distroless/static:nonroot WORKDIR / + +# Copy the binary and manifests data. COPY --from=builder /workspace/flux-operator . COPY data/ /data/ -USER 65532:65532 +# Run the operator as a non-root user. +USER 65532:65532 ENTRYPOINT ["/flux-operator"] diff --git a/Makefile b/Makefile index 40df070..ec5197d 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,14 @@ +# Makefile for building, testing, and deploying the Flux Operator. + # Image URL to use all building/pushing image targets -IMG ?= flux-operator:latest -# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. +IMG ?= ghcr.io/controlplaneio-fluxcd/flux-operator:latest + +# ENVTEST_K8S_VERSION refers to the version of kubebuilder +# assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.30.0 -# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +# Get the currently used golang install path +# (in GOPATH/bin, unless GOBIN is set). ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else @@ -11,9 +16,6 @@ GOBIN=$(shell go env GOBIN) endif # CONTAINER_TOOL defines the container tool to be used for building images. -# Be aware that the target commands are only tested with Docker which is -# scaffolded by default. However, you might want to replace it to use other -# tools. (i.e. podman) CONTAINER_TOOL ?= docker # Setting SHELL to bash allows bash commands to be executed by recipes. @@ -24,23 +26,6 @@ SHELL = /usr/bin/env bash -o pipefail .PHONY: all all: build -##@ General - -# The help target prints out all targets with their descriptions organized -# beneath their categories. The categories are represented by '##@' and the -# target descriptions by '##'. The awk command is responsible for reading the -# entire set of makefiles included in this invocation, looking for lines of the -# file as xyz: ## something, and then pretty-format the target and help. Then, -# if there's a line with ##@ something, that gets pretty-printed as a category. -# More info on the usage of ANSI control characters for terminal formatting: -# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters -# More info on the awk command: -# http://linuxcommand.org/lc3_adv_awk.php - -.PHONY: help -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) - ##@ Development .PHONY: manifests @@ -73,11 +58,11 @@ test-e2e: go test ./test/e2e/ -v -ginkgo.v .PHONY: lint -lint: golangci-lint ## Run golangci-lint linter +lint: golangci-lint ## Run golangci-lint linter. $(GOLANGCI_LINT) run .PHONY: lint-fix -lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes. $(GOLANGCI_LINT) run --fix ##@ Build @@ -107,7 +92,7 @@ docker-push: ## Push docker image with the manager. PLATFORMS ?= linux/arm64,linux/amd64 .PHONY: docker-buildx -docker-buildx: ## Build and push docker image for the manager for cross-platform support +docker-buildx: ## Build and push docker image for the manager for cross-platform support. # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - $(CONTAINER_TOOL) buildx create --name flux-operator-builder @@ -199,3 +184,9 @@ GOBIN=$(LOCALBIN) go install $${package} ;\ mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\ } endef + +##@ General + +.PHONY: help +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) diff --git a/internal/controller/fluxinstance_controller.go b/internal/controller/fluxinstance_controller.go index 0b98b67..e60d649 100644 --- a/internal/controller/fluxinstance_controller.go +++ b/internal/controller/fluxinstance_controller.go @@ -169,6 +169,8 @@ func (r *FluxInstanceReconciler) reconcile(ctx context.Context, return requeueAfter(obj), nil } +// build reads the distribution manifests from the storage path, +// matches the version and builds the final resources. func (r *FluxInstanceReconciler) build(ctx context.Context, obj *fluxcdv1.FluxInstance) (*builder.Result, error) { log := ctrl.LoggerFrom(ctx) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 3fa8014..965a968 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -10,8 +10,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/controlplaneio-fluxcd/flux-operator/test/utils" ) const ( @@ -26,16 +24,16 @@ var _ = BeforeSuite(func() { By("building the flux-operator image") cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", image)) - _, err = utils.Run(cmd) + _, err = Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred()) By("loading the flux-operator image on Kind") - err = utils.LoadImageToKindClusterWithName(image) + err = LoadImageToKindClusterWithName(image) ExpectWithOffset(1, err).NotTo(HaveOccurred()) By("deploying flux-operator") cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", image)) - _, err = utils.Run(cmd) + _, err = Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred()) By("validating that the flux-operator pod is running as expected") @@ -51,9 +49,9 @@ var _ = BeforeSuite(func() { "-n", namespace, ) - podOutput, err := utils.Run(cmd) + podOutput, err := Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) - podNames := utils.GetNonEmptyLines(string(podOutput)) + podNames := GetNonEmptyLines(string(podOutput)) if len(podNames) != 1 { return fmt.Errorf("expect 1 flux-operator pods running, but got %d", len(podNames)) } @@ -65,7 +63,7 @@ var _ = BeforeSuite(func() { "pods", controllerPodName, "-o", "jsonpath={.status.phase}", "-n", namespace, ) - status, err := utils.Run(cmd) + status, err := Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) if string(status) != "Running" { return fmt.Errorf("flux-operator pod in %s status", status) @@ -79,6 +77,6 @@ var _ = BeforeSuite(func() { var _ = AfterSuite(func() { By("uninstalling flux-operator") cmd := exec.Command("make", "undeploy") - _, err := utils.Run(cmd) + _, err := Run(cmd) Expect(err).NotTo(HaveOccurred()) }) diff --git a/test/e2e/instance_test.go b/test/e2e/instance_test.go index 4068951..d8be28d 100644 --- a/test/e2e/instance_test.go +++ b/test/e2e/instance_test.go @@ -9,8 +9,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/controlplaneio-fluxcd/flux-operator/test/utils" ) var _ = Describe("FluxInstance", Ordered, func() { @@ -21,13 +19,13 @@ var _ = Describe("FluxInstance", Ordered, func() { cmd := exec.Command("kubectl", "apply", "-k", "config/samples", "-n", namespace, ) - _, err := utils.Run(cmd) + _, err := Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) cmd = exec.Command("kubectl", "wait", "FluxInstance/flux", "-n", namespace, "--for=condition=Ready", "--timeout=5m", ) - _, err = utils.Run(cmd) + _, err = Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) return nil } @@ -42,19 +40,19 @@ var _ = Describe("FluxInstance", Ordered, func() { cmd := exec.Command("kubectl", "-n", namespace, "patch", "FluxInstance/flux", "--type=json", `-p=[{"op": "replace", "path": "/spec/cluster/multitenant", "value":true}]`, ) - _, err := utils.Run(cmd) + _, err := Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) cmd = exec.Command("kubectl", "wait", "FluxInstance/flux", "-n", namespace, "--for=condition=Ready", "--timeout=5m", ) - _, err = utils.Run(cmd) + _, err = Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) cmd = exec.Command("kubectl", "get", "deploy/kustomize-controller", "-n", namespace, "-o=yaml", ) - output, err := utils.Run(cmd) + output, err := Run(cmd) ExpectWithOffset(2, err).NotTo(HaveOccurred()) ExpectWithOffset(2, output).To(ContainSubstring("no-cross-namespace-refs=true")) @@ -69,16 +67,16 @@ var _ = Describe("FluxInstance", Ordered, func() { By("delete FluxInstance") cmd := exec.Command("kubectl", "delete", "-k", "config/samples", "--timeout=30s", "-n", namespace) - _, err := utils.Run(cmd) + _, err := Run(cmd) Expect(err).NotTo(HaveOccurred()) By("source-controller deleted") cmd = exec.Command("kubectl", "get", "deploy/source-controller", "-n", namespace) - _, err = utils.Run(cmd) + _, err = Run(cmd) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("not found")) By("namespace exists") cmd = exec.Command("kubectl", "get", "ns", namespace) - _, err = utils.Run(cmd) + _, err = Run(cmd) Expect(err).NotTo(HaveOccurred()) }) }) diff --git a/test/utils/utils.go b/test/e2e/utils.go similarity index 99% rename from test/utils/utils.go rename to test/e2e/utils.go index b504fc2..15d9e62 100644 --- a/test/utils/utils.go +++ b/test/e2e/utils.go @@ -1,7 +1,7 @@ // Copyright 2024 Stefan Prodan. // SPDX-License-Identifier: AGPL-3.0 -package utils +package e2e import ( "fmt"