diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..27d25f7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + ignore: + # Ignore Kubernetes dependencies to have full control on them. + - dependency-name: "k8s.io/*" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/docker/dev" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/docker/prod" + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7d49d16..0981948 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,15 +3,40 @@ name: CI on: [push] jobs: + check: + name: Check + runs-on: ubuntu-latest + container: golangci/golangci-lint:v1.56.2-alpine + steps: + - run: echo "machine github.com login ${CW_GO_DEPS_LOGIN} password ${CW_GO_DEPS_TOKEN}" > ~/.netrc + - uses: actions/checkout@v4 + - run: | + # We need this go flag because it started to error after golangci-lint is using Go 1.21. + # TODO(slok): Remove it on next (>1.54.1) golangci-lint upgrade to check if this problem has gone. + export GOFLAGS="-buildvcs=false" + ./scripts/check/check.sh + + unit-test: + name: Unit test + runs-on: ubuntu-latest + steps: + - run: echo "machine github.com login ${CW_GO_DEPS_LOGIN} password ${CW_GO_DEPS_TOKEN}" > ~/.netrc + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: make ci-test release-images: # Only on main branch. if: startsWith(github.ref, 'refs/heads/main') env: - IMAGE_NAME: ghcr.io/${GITHUB_REPOSITORY} + TAG_IMAGE_LATEST: "true" + PROD_IMAGE_NAME: ghcr.io/${GITHUB_REPOSITORY} VERSION: ${GITHUB_SHA} name: Release images runs-on: ubuntu-latest + needs: [check, unit-test] steps: - uses: actions/checkout@v3 - uses: docker/login-action@v2 @@ -20,15 +45,16 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and publish docker images - run: make build-publish-image + run: make build-publish-image-all tagged-release-images: # Only on tags. if: startsWith(github.ref, 'refs/tags/') env: - IMAGE_NAME: ghcr.io/${GITHUB_REPOSITORY} + PROD_IMAGE_NAME: ghcr.io/${GITHUB_REPOSITORY} name: Tagged release images runs-on: ubuntu-latest + needs: [check, unit-test] steps: - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} # Sets VERSION env var. - uses: actions/checkout@v3 @@ -38,4 +64,4 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and publish docker images - run: make build-publish-image \ No newline at end of file + run: make build-publish-image-all diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28be3b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Editors +.idea +.vscode + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Vendor directory +vendor/ + +# Test coverage. +.test_coverage.txt + +# Binaries +/bin diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..ebcc58a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,21 @@ +--- +run: + timeout: 3m + build-tags: + - integration + +linters: + enable: + - misspell + - goimports + - revive + - gofmt + #- depguard + - godot + +linters-settings: + revive: + rules: + # Spammy linter and complex to fix on lots of parameters. Makes more harm that it solves. + - name: unused-parameter + disabled: true \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7cf5d5a..74c89c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,16 @@ -FROM golang:1.20 -ARG CODEGEN_VERSION="1.27.0" -ARG CONTROLLER_GEN_VERSION="0.12.0" +FROM golang:1.22 AS build-stage +WORKDIR /src +COPY . . +RUN CGO_ENABLED=0 go build -o /usr/local/bin/kube-code-generator + +FROM golang:1.22 +ARG CODEGEN_VERSION="1.30.0-beta.0" +ARG CONTROLLER_GEN_VERSION="0.14.0" + + +COPY --from=build-stage /usr/local/bin/kube-code-generator /usr/local/bin/kube-code-generator RUN apt-get update && \ apt-get install -y \ @@ -10,23 +18,15 @@ RUN apt-get update && \ # Code generator stuff RUN wget http://github.com/kubernetes/code-generator/archive/kubernetes-${CODEGEN_VERSION}.tar.gz && \ - mkdir -p /go/src/k8s.io/code-generator/ && \ - tar zxvf kubernetes-${CODEGEN_VERSION}.tar.gz --strip 1 -C /go/src/k8s.io/code-generator/ && \ - rm kubernetes-${CODEGEN_VERSION}.tar.gz && \ - \ - wget http://github.com/kubernetes/apimachinery/archive/kubernetes-${CODEGEN_VERSION}.tar.gz && \ - mkdir -p /go/src/k8s.io/apimachinery/ && \ - tar zxvf kubernetes-${CODEGEN_VERSION}.tar.gz --strip 1 -C /go/src/k8s.io/apimachinery/ && \ - rm kubernetes-${CODEGEN_VERSION}.tar.gz && \ - \ - wget http://github.com/kubernetes/api/archive/kubernetes-${CODEGEN_VERSION}.tar.gz && \ - mkdir -p /go/src/k8s.io/api/ && \ - tar zxvf kubernetes-${CODEGEN_VERSION}.tar.gz --strip 1 -C /go/src/k8s.io/api/ && \ + mkdir -p /tmp/k8s-code-generator/ && \ + tar zxvf kubernetes-${CODEGEN_VERSION}.tar.gz --strip 1 -C /tmp/k8s-code-generator/ && \ + cd /tmp/k8s-code-generator/ && go mod tidy && cd - && \ rm kubernetes-${CODEGEN_VERSION}.tar.gz && \ \ wget https://github.com/kubernetes-sigs/controller-tools/archive/v${CONTROLLER_GEN_VERSION}.tar.gz && \ tar xvf ./v${CONTROLLER_GEN_VERSION}.tar.gz && \ cd ./controller-tools-${CONTROLLER_GEN_VERSION}/ && \ + go mod tidy && \ go build -o controller-gen ./cmd/controller-gen/ && \ mv ./controller-gen /usr/bin/ && \ rm -rf ../v${CONTROLLER_GEN_VERSION}.tar.gz && \ @@ -40,12 +40,9 @@ RUN addgroup --gid $gid codegen && \ adduser --gecos "First Last,RoomNumber,WorkPhone,HomePhone" --disabled-password --uid $uid --ingroup codegen codegen && \ chown codegen:codegen -R /go -COPY hack /hack -RUN chown codegen:codegen -R /hack && \ - mv /hack/* /usr/bin - USER codegen WORKDIR /usr/bin -CMD ["update-codegen.sh"] \ No newline at end of file +ENV KUBE_CODE_GENERATOR_CODEGEN_PATH=/tmp/k8s-code-generator +ENTRYPOINT ["kube-code-generator"] \ No newline at end of file diff --git a/Makefile b/Makefile index 8d3b322..e953a86 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,88 @@ - +SHELL := $(shell which bash) +OSTYPE := $(shell uname) +DOCKER := $(shell command -v docker) +GID := $(shell id -g) +UID := $(shell id -u) VERSION ?= $(shell git describe --tags --always) -default: build-image +UNIT_TEST_CMD := ./scripts/check/unit-test.sh +INTEGRATION_TEST_CMD := ./scripts/check/integration-test.sh +CHECK_CMD := ./scripts/check/check.sh + +DEV_IMAGE_NAME := localdev/kube-code-generator-dev +PROD_IMAGE_NAME ?= ghcr.io/slok/kube-code-generator + +DOCKER_RUN_CMD := docker run --env ostype=$(OSTYPE) -v ${PWD}:/src --rm ${DEV_IMAGE_NAME} +BUILD_BINARY_CMD := VERSION=${VERSION} ./scripts/build/bin/build.sh +BUILD_BINARY_ALL_CMD := VERSION=${VERSION} ./scripts/build/bin/build-all.sh +BUILD_DEV_IMAGE_CMD := IMAGE=${DEV_IMAGE_NAME} DOCKER_FILE_PATH=./docker/dev/Dockerfile VERSION=latest ./scripts/build/docker/build-image-dev.sh +BUILD_PROD_IMAGE_CMD := IMAGE=${PROD_IMAGE_NAME} DOCKER_FILE_PATH=./docker/prod/Dockerfile VERSION=${VERSION} ./scripts/build/docker/build-image.sh +BUILD_PUBLSIH_PROD_IMAGE_ALL_CMD := IMAGE=${PROD_IMAGE_NAME} DOCKER_FILE_PATH=./docker/prod/Dockerfile VERSION=${VERSION} ./scripts/build/docker/build-publish-image-all.sh +PUBLISH_PROD_IMAGE_CMD := IMAGE=${PROD_IMAGE_NAME} VERSION=${VERSION} ./scripts/build/docker/publish-image.sh -IMAGE_NAME ?= ghcr.io/slok/kube-code-generator -BUILD_IMAGE_CMD := IMAGE=${IMAGE_NAME} DOCKER_FILE_PATH=./Dockerfile VERSION=${VERSION} ./scripts/build-image.sh -BUILD_PUBLSIH_IMAGE_CMD := IMAGE=${IMAGE_NAME} DOCKER_FILE_PATH=./Dockerfile VERSION=${VERSION} ./scripts/build-publish-image.sh help: ## Show this help @echo "Help" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[93m %s\n", $$1, $$2}' +.PHONY: default +default: help + .PHONY: build-image -build-image: ## Builds the docker image. - @$(BUILD_IMAGE_CMD) +build-image: ## Builds the production docker image. + @$(BUILD_PROD_IMAGE_CMD) + +.PHONY: build-publish-image-all +build-publish-image-all: ## Builds and publishes all the production docker images (multiarch). + @$(BUILD_PUBLSIH_PROD_IMAGE_ALL_CMD) + +.PHONY: build-dev-image +build-dev-image: ## Builds the development docker image. + @$(BUILD_DEV_IMAGE_CMD) + +.PHONY: build +build: build-dev-image ## Builds the production binary. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(BUILD_BINARY_CMD)' + +.PHONY: build-all +build-all: build-dev-image ## Builds all archs production binaries. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(BUILD_BINARY_ALL_CMD)' + +.PHONY: test +test: build-dev-image ## Runs unit test. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(UNIT_TEST_CMD)' +.PHONY: check +check: build-dev-image ## Runs checks. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(CHECK_CMD)' + +.PHONY: integration +integration: build-dev-image ## Runs integration test. + @$(DOCKER_RUN_CMD) /bin/sh -c '$(INTEGRATION_TEST_CMD)' + +.PHONY: go-gen +go-gen: build-dev-image ## Generates go based code. + @$(DOCKER_RUN_CMD) /bin/sh -c './scripts/gogen.sh' + +.PHONY: gen +gen: go-gen ## Generates all. + +.PHONY: deps +deps: build-dev-image ## Fixes the dependencies. + @$(DOCKER_RUN_CMD) /bin/sh -c './scripts/deps.sh' + +.PHONY: ci-build +ci-build: ## Builds the production binary in CI environment (without docker). + @$(BUILD_BINARY_CMD) + +.PHONY: ci-unit-test +ci-test: ## Runs unit test in CI environment (without docker). + @$(UNIT_TEST_CMD) + +.PHONY: ci-check +ci-check: ## Runs checks in CI environment (without docker). + @$(CHECK_CMD) -.PHONY: build-publish-image -build-publish-image: ## Builds and publishes docker images. - @$(BUILD_PUBLSIH_IMAGE_CMD) \ No newline at end of file +.PHONY: ci-integration +ci-integration: ## Runs integraton test in CI environment (without docker). + @$(INTEGRATION_TEST_CMD) diff --git a/README.md b/README.md index 7f78a2b..87b6f8c 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,55 @@ # Kube code generator -![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.27-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) +![Kubernetes release](https://img.shields.io/badge/Kubernetes-v1.<30-green?logo=Kubernetes&style=flat&color=326CE5&logoColor=white) -A kubernetes code generator container that makes easier to generate CRD manifests and its Go clients. +## Introduction -Uses [official code-generator](https://github.com/kubernetes/code-generator) created by Kubernetes to autogenerate the code required for the CRDs. +When we speak about Kubernetes operators or controllers, normally Go code, CR, CRDs... are required. To create all the autogenerated Kubernetes Go code (Clients, helpers...) and manifests (CRD), the process is a bit painful. -## Generation targets +This small project tries making easy this process, by creating a small layer between Kubernetes official tooling that are used to get all this autogenerated stuff, and abstract options and infer some others, making a better UX for the user. -- CRD based Go code (clients, lib...). -- CRD manifest YAMLs to register your CRs on the cluster. +The projects that are used under the hood are: -## Docker image versions +- [code-generator](https://github.com/kubernetes/code-generator) for Go code autogeneration. +- [controller-tools](https://github.com/kubernetes-sigs/controller-tools) for CRD autogeneration. -| | Docker image | -| ---------------- | ------------------------------------------------------- | -| Kubernetes v1.27 | `docker pull ghcr.io/slok/kube-code-generator:v1.27.0` | -| Kubernetes v1.26 | `docker pull ghcr.io/slok/kube-code-generator:v1.26.0` | -| Kubernetes v1.25 | `docker pull ghcr.io/slok/kube-code-generator:v1.25.0` | -| Kubernetes v1.24 | `docker pull ghcr.io/slok/kube-code-generator:v1.24.0` | -| Kubernetes v1.23 | `docker pull ghcr.io/slok/kube-code-generator:v1.23.0` | -| Kubernetes v1.22 | `docker pull ghcr.io/slok/kube-code-generator:v1.22.0` | -| Kubernetes v1.21 | `docker pull ghcr.io/slok/kube-code-generator:v1.21.1` | -| Kubernetes v1.20 | `docker pull ghcr.io/slok/kube-code-generator:v1.20.1` | -| Kubernetes v1.19 | `docker pull ghcr.io/slok/kube-code-generator:v1.19.2` | -| Kubernetes v1.18 | `docker pull ghcr.io/slok/kube-code-generator:v1.18.0` | -| Kubernetes v1.17 | `docker pull ghcr.io/slok/kube-code-generator:v1.17.3` | -| Kubernetes v1.16 | `docker pull ghcr.io/slok/kube-code-generator:v1.16.7` | -| Kubernetes v1.15 | `docker pull ghcr.io/slok/kube-code-generator:v1.15.10` | -| Kubernetes v1.14 | `docker pull ghcr.io/slok/kube-code-generator:v1.14.2` | -| Kubernetes v1.13 | `docker pull ghcr.io/slok/kube-code-generator:v1.13.5` | -| Kubernetes v1.12 | `docker pull ghcr.io/slok/kube-code-generator:v1.12.4` | -| Kubernetes v1.11 | `docker pull ghcr.io/slok/kube-code-generator:v1.11.3` | -| Kubernetes v1.10 | `docker pull ghcr.io/slok/kube-code-generator:v1.10.0` | -| Kubernetes v1.9 | `docker pull ghcr.io/slok/kube-code-generator:v1.9.1` | +## Why and when use this -## Getting started +- You don't like, need or use kubebuilder for your CRDs. +- You want simple tooling to generate Kubernetes CRD Go clients and manifests. +- You like safe standards and simple things. +- You use CRDs for more/other things than operators (e.g: generating CLIs, storing state on k8s as APIs...). +- You don't want to do hacky and ugly stuff to start creating Kubernetes tooling. -The best way to know how to use it is by checking the [example](example/) that will generate the required clients and CRD manifests. +## Features -### Optional settings +- Small API/configuration. +- Safe standards +- Ready to use Docker images. +- Generates Go code like clients and informers (Used to implement operators, CLIs...). +- Generates CRD manifests (Used for API registration on k8s clusters). -Some settings are optional so you can customize special cases: +## How to use it -- On CRD manifest YAML generation: - - `CRD_FLAG` env var to overwrite CRD flag with a custom one. (E.g: Use `allowDangerousTypes=true` to allow `float64` on generation, [more info here][unsecure-float64]) +The easiest way is to use the provided Docker image as it has all the required upstream dependencies. -[unsecure-float64]: https://github.com/kubernetes-sigs/controller-tools/issues/245 +Here is an example that mounts the current directory (a Go project) and generates the Go code and the CRDs by providing the APIs input directory and the generation output directories: + +```bash +docker run -it --rm -v ${PWD}:/app ghcr.io/slok/kube-code-generator \ + --apis-in ./apis \ + --go-gen-out ./gen \ + --crd-gen-out ./gen/manifests +``` + +However, the best way to know how to use it is with a full example, you have it in [_example](_example/) dir. + +## Kubernetes versions + +| Kubernetes | Docker image | +| ---------- | ------------------------------------------------------- | +| v1.29 | `docker pull ghcr.io/slok/kube-code-generator:v1.29.0` | + +### Versions =v1.29` and changes the usage of the app, so for other versions you will need to check the previous implementation I would suggest that you check the [Readme of `=1.16 we use gomod, so we need to execute from the project directory. -cd "${GOPATH}/src/${PROJECT_PACKAGE}" - -# Ugly but needs to be relative if we want to use k8s.io/code-generator -# as it is without touching/sed-ing the code/scripts -RELATIVE_ROOT_PATH=$(realpath --relative-to="${PWD}" /) -CODEGEN_PKG=${RELATIVE_ROOT_PATH}${GOPATH}/src/k8s.io/code-generator - -BOILERPLATE_PATH=/tmp/fake-boilerplate.txt -touch "${BOILERPLATE_PATH}" - -# Only generate deepcopy (runtime object needs) and typed client. -# Typed listers & informers not required for the moment. Used with generic -# custom informer/listerwatchers. -${CODEGEN_PKG}/generate-groups.sh \ - ${GENERATION_TARGETS} \ - ${CLIENT_GENERATOR_OUT} \ - ${APIS_ROOT} \ - "${GROUPS_VERSION}" \ - "--go-header-file=${BOILERPLATE_PATH}" diff --git a/hack/update-crd.sh b/hack/update-crd.sh deleted file mode 100755 index 39e7edd..0000000 --- a/hack/update-crd.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -eufo pipefail - -GO_PROJECT_ROOT="${GO_PROJECT_ROOT:-""}" -CRD_TYPES_PATH="${CRD_TYPES_PATH:-""}" -CRD_OUT_PATH="${CRD_OUT_PATH:-""}" -CRD_FLAG="${CRD_FLAG:-"crd:crdVersions=v1"}" - -[ -z "$GO_PROJECT_ROOT" ] && echo "GO_PROJECT_ROOT env var is required" && exit 1 -[ -z "$CRD_TYPES_PATH" ] && echo "CRD_TYPES_PATH env var is required" && exit 1 -[ -z "$CRD_OUT_PATH" ] && echo "CRD_OUT_PATH env var is required" && exit 1 - -GO_PROJECT_ROOT=$(realpath ${GO_PROJECT_ROOT}) -CRD_TYPES_PATH=$(realpath ${CRD_TYPES_PATH}) -CRD_OUT_PATH=$(realpath ${CRD_OUT_PATH}) - -cd ${GO_PROJECT_ROOT} - -# Needs relative paths. -CRD_TYPES_PATH=$(realpath --relative-to="${PWD}" ${CRD_TYPES_PATH}) -CRD_OUT_PATH=$(realpath --relative-to="${PWD}" ${CRD_OUT_PATH}) - -mkdir -p ${CRD_OUT_PATH} -echo "Generating CRD manifests..." - -controller-gen \ - "${CRD_FLAG}" \ - paths="./${CRD_TYPES_PATH}/..." \ - output:dir="./${CRD_OUT_PATH}" diff --git a/internal/generate/client.go b/internal/generate/client.go new file mode 100644 index 0000000..5acf5b2 --- /dev/null +++ b/internal/generate/client.go @@ -0,0 +1,69 @@ +package generate + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/slok/kube-code-generator/internal/log" +) + +type ClientGenerator struct { + cmdArgs []string + codeGenPath string + apisPath string + exec BashExecutor + logger log.Logger +} + +func NewClientGenerator(logger log.Logger, codeGenPath string, exec BashExecutor) *ClientGenerator { + if exec == nil { + exec = StdBashExecutor + } + + return &ClientGenerator{ + codeGenPath: codeGenPath, + exec: exec, + logger: logger, + } +} + +func (g *ClientGenerator) WithWatch() *ClientGenerator { + g.cmdArgs = append(g.cmdArgs, `--with-watch`) + return g +} + +func (g *ClientGenerator) WithOutputPkg(pkg string) *ClientGenerator { + g.cmdArgs = append(g.cmdArgs, `--output-pkg`, pkg) + return g +} + +func (g *ClientGenerator) WithOutputDir(path string) *ClientGenerator { + g.cmdArgs = append(g.cmdArgs, `--output-dir`, path) + return g +} + +func (g *ClientGenerator) WithBoilerplate(path string) *ClientGenerator { + g.cmdArgs = append(g.cmdArgs, `--boilerplate`, path) + return g +} + +func (g *ClientGenerator) WithAPIsPath(path string) *ClientGenerator { + g.apisPath = path + return g +} + +func (g *ClientGenerator) Run(ctx context.Context) error { + kubeCodeGenSHPath := filepath.Join(g.codeGenPath, "kube_codegen.sh") + bashCmd := fmt.Sprintf("source %s ; kube::codegen::gen_client %s %s", kubeCodeGenSHPath, strings.Join(g.cmdArgs, " "), g.apisPath) + + g.logger.Debugf("Command executed: %s", bashCmd) + out, err := g.exec.BashExec(ctx, bashCmd) + if err != nil { + return fmt.Errorf("error while executing bash script: %w", err) + } + g.logger.Debugf("Command output: %s", string(out)) + + return nil +} diff --git a/internal/generate/client_test.go b/internal/generate/client_test.go new file mode 100644 index 0000000..4116f31 --- /dev/null +++ b/internal/generate/client_test.go @@ -0,0 +1,48 @@ +package generate_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + + "github.com/slok/kube-code-generator/internal/generate" + "github.com/slok/kube-code-generator/internal/generate/generatemock" + "github.com/slok/kube-code-generator/internal/log" +) + +func TestClientGenerator(t *testing.T) { + tests := map[string]struct { + exec func(g *generate.ClientGenerator) + expCmd string + }{ + "Without options.": { + exec: func(g *generate.ClientGenerator) { _ = g.Run(context.TODO()) }, + expCmd: `source kube_codegen.sh ; kube::codegen::gen_client `, + }, + + "Regular options.": { + exec: func(g *generate.ClientGenerator) { + _ = g.WithAPIsPath("./apis"). + WithBoilerplate("./boilerplate.txt"). + WithOutputDir("./out"). + WithOutputPkg("my-pkg"). + WithWatch(). + Run(context.TODO()) + }, + expCmd: `source kube_codegen.sh ; kube::codegen::gen_client --boilerplate ./boilerplate.txt --output-dir ./out --output-pkg my-pkg --with-watch ./apis`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m := generatemock.NewBashExecutor(t) + m.On("BashExec", mock.Anything, test.expCmd).Once().Return("", nil) + + g := generate.NewClientGenerator(log.Noop, "", m) + test.exec(g) + + m.AssertExpectations(t) + }) + } +} diff --git a/internal/generate/crd.go b/internal/generate/crd.go new file mode 100644 index 0000000..8f16cb0 --- /dev/null +++ b/internal/generate/crd.go @@ -0,0 +1,82 @@ +package generate + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/slok/kube-code-generator/internal/log" +) + +type CRDGenerator struct { + controllerGenBin string + apisPath string + outPath string + crdOptions []string + exec BashExecutor + logger log.Logger +} + +const strSeparator = string(filepath.Separator) + +func NewCRDGenerator(logger log.Logger, controllerGenBin string, exec BashExecutor) *CRDGenerator { + if exec == nil { + exec = StdBashExecutor + } + + if controllerGenBin == "" { + controllerGenBin = "controller-gen" + } + + return &CRDGenerator{ + controllerGenBin: controllerGenBin, + crdOptions: []string{ + "crdVersions=v1", // Only one supported for now. + }, + exec: exec, + logger: logger, + } +} + +func (g *CRDGenerator) WithAllowDangerousTypes() *CRDGenerator { + g.crdOptions = append(g.crdOptions, "allowDangerousTypes=true") + return g +} + +func (g *CRDGenerator) WithIgnoreUnexportedFields() *CRDGenerator { + g.crdOptions = append(g.crdOptions, "ignoreUnexportedFields=true") + return g +} + +func (g *CRDGenerator) WithIgnoreDescription() *CRDGenerator { + g.crdOptions = append(g.crdOptions, "maxDescLen=0") + return g +} + +func (g *CRDGenerator) WithOutputDir(path string) *CRDGenerator { + path = filepath.Clean(path) + g.outPath = fmt.Sprintf(".%s%s", strSeparator, path) // We need `./` in front of it. + return g +} + +func (g *CRDGenerator) WithAPIsPath(path string) *CRDGenerator { + path = filepath.Clean(path) + g.apisPath = fmt.Sprintf(".%s%s%s...", strSeparator, path, strSeparator) // We need `./` in front of it. + return g +} + +func (g *CRDGenerator) Run(ctx context.Context) error { + paths := fmt.Sprintf(`paths="%s"`, g.apisPath) + outputDir := fmt.Sprintf(`output:dir="%s"`, g.outPath) + crds := fmt.Sprintf("crd:%s", strings.Join(g.crdOptions, ",")) + bashCmd := fmt.Sprintf("%s %s %s %s", g.controllerGenBin, paths, outputDir, crds) + + g.logger.Debugf("Command executed: %s", bashCmd) + out, err := g.exec.BashExec(ctx, bashCmd) + if err != nil { + return fmt.Errorf("error while executing bash script: %w", err) + } + g.logger.Debugf("Command output: %s", string(out)) + return nil +} diff --git a/internal/generate/crd_test.go b/internal/generate/crd_test.go new file mode 100644 index 0000000..7393f03 --- /dev/null +++ b/internal/generate/crd_test.go @@ -0,0 +1,48 @@ +package generate_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + + "github.com/slok/kube-code-generator/internal/generate" + "github.com/slok/kube-code-generator/internal/generate/generatemock" + "github.com/slok/kube-code-generator/internal/log" +) + +func TestCRDGenerator(t *testing.T) { + tests := map[string]struct { + exec func(g *generate.CRDGenerator) + expCmd string + }{ + "Without options.": { + exec: func(g *generate.CRDGenerator) { _ = g.Run(context.TODO()) }, + expCmd: `controller-gen paths="" output:dir="" crd:crdVersions=v1`, + }, + + "Regular options.": { + exec: func(g *generate.CRDGenerator) { + _ = g.WithAPIsPath("./apis"). + WithOutputDir("./out"). + WithAllowDangerousTypes(). + WithIgnoreDescription(). + WithIgnoreUnexportedFields(). + Run(context.TODO()) + }, + expCmd: `controller-gen paths="./apis/..." output:dir="./out" crd:crdVersions=v1,allowDangerousTypes=true,maxDescLen=0,ignoreUnexportedFields=true`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m := generatemock.NewBashExecutor(t) + m.On("BashExec", mock.Anything, test.expCmd).Once().Return("", nil) + + g := generate.NewCRDGenerator(log.Noop, "", m) + test.exec(g) + + m.AssertExpectations(t) + }) + } +} diff --git a/internal/generate/generate.go b/internal/generate/generate.go new file mode 100644 index 0000000..134166c --- /dev/null +++ b/internal/generate/generate.go @@ -0,0 +1,28 @@ +package generate + +import ( + "context" + "fmt" + "os/exec" +) + +type BashExecutor interface { + BashExec(ctx context.Context, bashCmd string) (string, error) +} + +//go:generate mockery --case underscore --output generatemock --outpkg generatemock --name BashExecutor + +// StdBashExecutor is an standard bash executor. +var StdBashExecutor = stdBashExecutor(false) + +type stdBashExecutor bool + +func (stdBashExecutor) BashExec(ctx context.Context, bashCmd string) (string, error) { + cmd := exec.CommandContext(ctx, "bash", "-c", bashCmd) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("%w: %s", err, string(out)) + } + + return string(out), nil +} diff --git a/internal/generate/generatemock/bash_executor.go b/internal/generate/generatemock/bash_executor.go new file mode 100644 index 0000000..2fffd9a --- /dev/null +++ b/internal/generate/generatemock/bash_executor.go @@ -0,0 +1,56 @@ +// Code generated by mockery v2.42.1. DO NOT EDIT. + +package generatemock + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// BashExecutor is an autogenerated mock type for the BashExecutor type +type BashExecutor struct { + mock.Mock +} + +// BashExec provides a mock function with given fields: ctx, bashCmd +func (_m *BashExecutor) BashExec(ctx context.Context, bashCmd string) (string, error) { + ret := _m.Called(ctx, bashCmd) + + if len(ret) == 0 { + panic("no return value specified for BashExec") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return rf(ctx, bashCmd) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, bashCmd) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, bashCmd) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewBashExecutor creates a new instance of BashExecutor. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBashExecutor(t interface { + mock.TestingT + Cleanup(func()) +}) *BashExecutor { + mock := &BashExecutor{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/generate/helpers.go b/internal/generate/helpers.go new file mode 100644 index 0000000..f4b2fa2 --- /dev/null +++ b/internal/generate/helpers.go @@ -0,0 +1,54 @@ +package generate + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/slok/kube-code-generator/internal/log" +) + +type HelpersGenerator struct { + cmdArgs []string + codeGenPath string + apisPath string + exec BashExecutor + logger log.Logger +} + +func NewHelpersGenerator(logger log.Logger, codeGenPath string, exec BashExecutor) *HelpersGenerator { + if exec == nil { + exec = StdBashExecutor + } + + return &HelpersGenerator{ + codeGenPath: codeGenPath, + exec: exec, + logger: logger, + } +} + +func (g *HelpersGenerator) WithBoilerplate(path string) *HelpersGenerator { + g.cmdArgs = append(g.cmdArgs, `--boilerplate`, path) + return g +} + +func (g *HelpersGenerator) WithAPIsPath(path string) *HelpersGenerator { + g.apisPath = path + return g +} + +func (g *HelpersGenerator) Run(ctx context.Context) error { + kubeCodeGenSHPath := filepath.Join(g.codeGenPath, "kube_codegen.sh") + bashCmd := fmt.Sprintf("source %s ; kube::codegen::gen_helpers %s %s", kubeCodeGenSHPath, strings.Join(g.cmdArgs, " "), g.apisPath) + + g.logger.Debugf("Command executed: %s", bashCmd) + out, err := g.exec.BashExec(ctx, bashCmd) + if err != nil { + return fmt.Errorf("error while executing bash script: %w", err) + } + g.logger.Debugf("Command output: %s", string(out)) + + return nil +} diff --git a/internal/generate/helpers_test.go b/internal/generate/helpers_test.go new file mode 100644 index 0000000..165021c --- /dev/null +++ b/internal/generate/helpers_test.go @@ -0,0 +1,45 @@ +package generate_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/mock" + + "github.com/slok/kube-code-generator/internal/generate" + "github.com/slok/kube-code-generator/internal/generate/generatemock" + "github.com/slok/kube-code-generator/internal/log" +) + +func TestHelpersGenerator(t *testing.T) { + tests := map[string]struct { + exec func(g *generate.HelpersGenerator) + expCmd string + }{ + "Without options.": { + exec: func(g *generate.HelpersGenerator) { _ = g.Run(context.TODO()) }, + expCmd: `source kube_codegen.sh ; kube::codegen::gen_helpers `, + }, + + "Regular options.": { + exec: func(g *generate.HelpersGenerator) { + _ = g.WithAPIsPath("./apis"). + WithBoilerplate("./boilerplate.txt"). + Run(context.TODO()) + }, + expCmd: `source kube_codegen.sh ; kube::codegen::gen_helpers --boilerplate ./boilerplate.txt ./apis`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m := generatemock.NewBashExecutor(t) + m.On("BashExec", mock.Anything, test.expCmd).Once().Return("", nil) + + g := generate.NewHelpersGenerator(log.Noop, "", m) + test.exec(g) + + m.AssertExpectations(t) + }) + } +} diff --git a/internal/info/info.go b/internal/info/info.go new file mode 100644 index 0000000..76a10dc --- /dev/null +++ b/internal/info/info.go @@ -0,0 +1,4 @@ +package info + +// Version is the application version (normally set when building the binary). +var Version = "dev" diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..fae1635 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,66 @@ +package log + +import "context" + +// Kv is a helper type for structured logging fields usage. +type Kv = map[string]interface{} + +// Logger is the interface that the loggers use. +type Logger interface { + Infof(format string, args ...interface{}) + Warningf(format string, args ...interface{}) + Errorf(format string, args ...interface{}) + Debugf(format string, args ...interface{}) + WithValues(values map[string]interface{}) Logger + WithCtxValues(ctx context.Context) Logger + SetValuesOnCtx(parent context.Context, values map[string]interface{}) context.Context +} + +// Noop logger doesn't log anything. +const Noop = noop(false) + +type noop bool + +func (n noop) Infof(format string, args ...interface{}) {} +func (n noop) Warningf(format string, args ...interface{}) {} +func (n noop) Errorf(format string, args ...interface{}) {} +func (n noop) Debugf(format string, args ...interface{}) {} +func (n noop) WithValues(map[string]interface{}) Logger { return n } +func (n noop) WithCtxValues(context.Context) Logger { return n } +func (n noop) SetValuesOnCtx(parent context.Context, values Kv) context.Context { return parent } + +type contextKey string + +// contextLogValuesKey used as unique key to store log values in the context. +const contextLogValuesKey = contextKey("internal-log") + +// CtxWithValues returns a copy of parent in which the key values passed have been +// stored ready to be used using log.Logger. +func CtxWithValues(parent context.Context, kv Kv) context.Context { + // Maybe we have values already set. + oldValues, ok := parent.Value(contextLogValuesKey).(Kv) + if !ok { + oldValues = Kv{} + } + + // Copy old and received values into the new kv. + newValues := Kv{} + for k, v := range oldValues { + newValues[k] = v + } + for k, v := range kv { + newValues[k] = v + } + + return context.WithValue(parent, contextLogValuesKey, newValues) +} + +// ValuesFromCtx gets the log Key values from a context. +func ValuesFromCtx(ctx context.Context) Kv { + values, ok := ctx.Value(contextLogValuesKey).(Kv) + if !ok { + return Kv{} + } + + return values +} diff --git a/internal/log/logrus/logrus.go b/internal/log/logrus/logrus.go new file mode 100644 index 0000000..c4c62c6 --- /dev/null +++ b/internal/log/logrus/logrus.go @@ -0,0 +1,31 @@ +package logrus + +import ( + "context" + + "github.com/sirupsen/logrus" + + "github.com/slok/kube-code-generator/internal/log" +) + +type logger struct { + *logrus.Entry +} + +// NewLogrus returns a new log.Logger for a logrus implementation. +func NewLogrus(l *logrus.Entry) log.Logger { + return logger{Entry: l} +} + +func (l logger) WithValues(kv log.Kv) log.Logger { + newLogger := l.Entry.WithFields(kv) + return NewLogrus(newLogger) +} + +func (l logger) WithCtxValues(ctx context.Context) log.Logger { + return l.WithValues(log.ValuesFromCtx(ctx)) +} + +func (l logger) SetValuesOnCtx(parent context.Context, values log.Kv) context.Context { + return log.CtxWithValues(parent, values) +} diff --git a/internal/util/gomod/gomod.go b/internal/util/gomod/gomod.go new file mode 100644 index 0000000..b0ea9d2 --- /dev/null +++ b/internal/util/gomod/gomod.go @@ -0,0 +1,35 @@ +package gomod + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" +) + +var ( + goModuleRegexp = regexp.MustCompile(`(?m)^module ([^\s]+)$`) +) + +// GetGoModule will return the go module declaration from a go mod file content. +func GetGoModule(goModFileContent string) (string, error) { + match := goModuleRegexp.FindAllStringSubmatch(goModFileContent, 1) + if len(match) < 1 || len(match[0]) < 2 { + return "", fmt.Errorf(`could not find module declaration on "go.mod"`) + } + packageName := match[0][1] + + return packageName, nil +} + +// GetImportPackageFromDir will return the go package based on a go project module and a relative directory. +func GetGoPackageFromDir(goModule, relativeDir string) string { + pkg := strings.TrimSuffix(relativeDir, "/") + "/" // Ensure slash. + pkg = filepath.Dir(pkg) + if pkg != "." && pkg != "" { + return goModule + "/" + pkg + } + + return pkg + +} diff --git a/internal/util/gomod/gomod_test.go b/internal/util/gomod/gomod_test.go new file mode 100644 index 0000000..f32a977 --- /dev/null +++ b/internal/util/gomod/gomod_test.go @@ -0,0 +1,102 @@ +package gomod_test + +import ( + "testing" + + "github.com/slok/kube-code-generator/internal/util/gomod" + "github.com/stretchr/testify/assert" +) + +func TestGetGoModule(t *testing.T) { + tests := map[string]struct { + goDotMod string + expectedGoMod string + expErr bool + }{ + "If not content, it should fail.": { + goDotMod: "", + expErr: true, + }, + + "If there is content, but no go module, it should fail.": { + goDotMod: ` +go 1.22.0 + +require ( + github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.2 +) + +`, + expErr: true, + }, + + "If there is content, and go module, it should return the module.": { + goDotMod: ` +module github.com/slok/kube-code-generator + +go 1.22.0 + +require ( + github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.2 +) + +require ( + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +)`, + expectedGoMod: "github.com/slok/kube-code-generator", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + gotGoMod, err := gomod.GetGoModule(test.goDotMod) + + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expectedGoMod, gotGoMod) + } + }) + } +} + +func TestGetGoPackageFromDir(t *testing.T) { + tests := map[string]struct { + goMod string + pkgDir string + expectedPkg string + }{ + "Having a go module and a package dir, it should return the Go package.": { + goMod: "github.com/slok/kube-code-generator/example", + pkgDir: "./something/gen/otherthing", + expectedPkg: "github.com/slok/kube-code-generator/example/something/gen/otherthing", + }, + + "Having a go module and a package dir, it should return the Go package (not relative dir prefix).": { + goMod: "github.com/slok/kube-code-generator/example", + pkgDir: "something/gen/otherthing", + expectedPkg: "github.com/slok/kube-code-generator/example/something/gen/otherthing", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + gotGoPkg := gomod.GetGoPackageFromDir(test.goMod, test.pkgDir) + assert.Equal(test.expectedPkg, gotGoPkg) + + }) + } +} diff --git a/scripts/build-image.sh b/scripts/build-image.sh deleted file mode 100755 index 004f38b..0000000 --- a/scripts/build-image.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env sh - -set -e - -[ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1 -[ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1 -[ -z "$DOCKER_FILE_PATH" ] && echo "DOCKER_FILE_PATH env var is required." && exit 1 - -IMAGE_TAG="${IMAGE}:${VERSION}" - -# Build image. -echo "Building image ${IMAGE_TAG}..." -docker build \ - --build-arg VERSION="${VERSION}" \ - -t "${IMAGE_TAG}" \ - -f "${DOCKER_FILE_PATH}" . diff --git a/scripts/build-publish-image.sh b/scripts/build-publish-image.sh deleted file mode 100755 index 5bef981..0000000 --- a/scripts/build-publish-image.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o nounset - -[ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1 -[ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1 - -./scripts/build-image.sh -./scripts/publish-image.sh diff --git a/scripts/build/bin/build-all.sh b/scripts/build/bin/build-all.sh new file mode 100755 index 0000000..874e34c --- /dev/null +++ b/scripts/build/bin/build-all.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset + +# Build all. +ostypes=("Linux" "Darwin" "Windows" "ARM") +for ostype in "${ostypes[@]}" +do + ostype="${ostype}" ./scripts/build/bin/build.sh +done + +# Create checksums. +checksums_dir="./bin" +cd ${checksums_dir} && sha256sum * > ./checksums.txt diff --git a/scripts/build/bin/build-raw.sh b/scripts/build/bin/build-raw.sh new file mode 100755 index 0000000..1eabd9a --- /dev/null +++ b/scripts/build/bin/build-raw.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset + +# Env vars that can be set. +# - EXTENSION: The binary out extension. +# - VERSION: Version for the binary. +# - GOOS: OS compiling target +# - GOARCH: Arch compiling target. +# - GOARM: ARM version. + +version_path="github.com/slok/kube-code-generator/internal/info.Version" +src=./cmd/kube-code-generator +out=./bin/kube-code-generator + +# Prepare flags. +final_out=${out}${EXTENSION:-} +ldf_cmp="-s -w -extldflags '-static'" +f_ver="-X ${version_path}=${VERSION:-dev}" + +# Build binary. +echo "[*] Building binary at ${final_out} (GOOS=${GOOS:-}, GOARCH=${GOARCH:-}, GOARM=${GOARM:-}, VERSION=${VERSION:-}, EXTENSION=${EXTENSION:-})" +CGO_ENABLED=0 go build -o ${final_out} --ldflags "${ldf_cmp} ${f_ver}" -buildvcs=false ${src} diff --git a/scripts/build/bin/build.sh b/scripts/build/bin/build.sh new file mode 100755 index 0000000..511f361 --- /dev/null +++ b/scripts/build/bin/build.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset + +build_script="./scripts/build/bin/build-raw.sh" +ostype=${ostype:-"native"} + +echo "[+] Build OS type selected: ${ostype}" + +if [ $ostype == 'Linux' ]; then + EXTENSION="-linux-amd64" GOOS="linux" GOARCH="amd64" ${build_script} +elif [ $ostype == 'Darwin' ]; then + EXTENSION="-darwin-amd64" GOOS="darwin" GOARCH="amd64" ${build_script} + EXTENSION="-darwin-arm64" GOOS="darwin" GOARCH="arm64" ${build_script} +elif [ $ostype == 'Windows' ]; then + EXTENSION="-windows-amd64.exe" GOOS="windows" GOARCH="amd64" ${build_script} +elif [ $ostype == 'ARM' ]; then + EXTENSION="-linux-arm64" GOOS="linux" GOARCH="arm64" ${build_script} + EXTENSION="-linux-arm-v7" GOOS="linux" GOARCH="arm" GOARM="7" ${build_script} +else + # Native. + ${build_script} +fi diff --git a/scripts/build/docker/build-image-dev.sh b/scripts/build/docker/build-image-dev.sh new file mode 100755 index 0000000..5ab6109 --- /dev/null +++ b/scripts/build/docker/build-image-dev.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh + +set -e + +[ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1 +[ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1 +[ -z "$DOCKER_FILE_PATH" ] && echo "DOCKER_FILE_PATH env var is required." && exit 1 +[ -z "$CW_GO_DEPS_LOGIN" ] && echo "CW_GO_DEPS_LOGIN env var is required (go deps)." && exit 1 +[ -z "$CW_GO_DEPS_TOKEN" ] && echo "CW_GO_DEPS_TOKEN env var is required (go deps)." && exit 1 + +# Build image. +echo "Building dev image ${IMAGE}:${VERSION}..." +docker build \ + --build-arg CW_GO_DEPS_LOGIN="${CW_GO_DEPS_LOGIN}" \ + --build-arg CW_GO_DEPS_TOKEN="${CW_GO_DEPS_TOKEN}" \ + -t "${IMAGE}:${VERSION}" \ + -f "${DOCKER_FILE_PATH}" . diff --git a/scripts/build/docker/build-image.sh b/scripts/build/docker/build-image.sh new file mode 100755 index 0000000..07eb83b --- /dev/null +++ b/scripts/build/docker/build-image.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env sh + +set -e + +[ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1 +[ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1 +[ -z "$DOCKER_FILE_PATH" ] && echo "DOCKER_FILE_PATH env var is required." && exit 1 +[ -z "$CW_GO_DEPS_LOGIN" ] && echo "CW_GO_DEPS_LOGIN env var is required (go deps)." && exit 1 +[ -z "$CW_GO_DEPS_TOKEN" ] && echo "CW_GO_DEPS_TOKEN env var is required (go deps)." && exit 1 + +# By default use amd64 architecture. +DEF_ARCH=amd64 +ARCH=${ARCH:-$DEF_ARCH} + +IMAGE_TAG_ARCH="${IMAGE}:${VERSION}-${ARCH}" + +# Build image. +echo "Building image ${IMAGE_TAG_ARCH}..." +docker build \ + --build-arg CW_GO_DEPS_LOGIN="${CW_GO_DEPS_LOGIN}" \ + --build-arg CW_GO_DEPS_TOKEN="${CW_GO_DEPS_TOKEN}" \ + --build-arg VERSION="${VERSION}" \ + --build-arg ARCH="${ARCH}" \ + -t "${IMAGE_TAG_ARCH}" \ + -f "${DOCKER_FILE_PATH}" . diff --git a/scripts/build/docker/build-publish-image-all.sh b/scripts/build/docker/build-publish-image-all.sh new file mode 100755 index 0000000..0d09307 --- /dev/null +++ b/scripts/build/docker/build-publish-image-all.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset + +[ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1 +[ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1 + +function build_and_publish() { + local arch="${1}" + ARCH="${arch}" ./scripts/build/docker/build-image.sh + ARCH="${arch}" ./scripts/build/docker/publish-image.sh +} + +# Build and publish images for all architectures. +#archs=("amd64" "arm64" "arm" "ppc64le" "s390x") +archs=("amd64") +for arch in "${archs[@]}"; do + build_and_publish "${arch}" +done + +IMAGE_TAG="${IMAGE}:${VERSION}" + +# Create manifest to join all arch images under one virtual tag. +MANIFEST="docker manifest create -a ${IMAGE_TAG}" +for arch in "${archs[@]}"; do + MANIFEST="${MANIFEST} ${IMAGE_TAG}-${arch}" +done +eval "${MANIFEST}" + +# Annotate each arch manifest to set which image is build for which CPU architecture. +for arch in "${archs[@]}"; do + docker manifest annotate --arch "${arch}" "${IMAGE_TAG}" "${IMAGE_TAG}-${arch}" +done + +# Push virual tag metadata. +docker manifest push "${IMAGE_TAG}" + +# Same as the regular virtual tag but for `:latest`. +if [ ! -z "${TAG_IMAGE_LATEST:-}" ]; then + IMAGE_TAG_LATEST="${IMAGE}:latest" + + # Clean latest manifest in case there is one. + docker manifest rm ${IMAGE_TAG_LATEST} || true + + # Create manifest to join all arch images under one virtual tag. + MANIFEST_LATEST="docker manifest create -a ${IMAGE_TAG_LATEST}" + for arch in "${archs[@]}"; do + MANIFEST_LATEST="${MANIFEST_LATEST} ${IMAGE_TAG}-${arch}" + done + eval "${MANIFEST_LATEST}" + + # Annotate each arch manifest to set which image is build for which CPU architecture. + for arch in "${archs[@]}"; do + docker manifest annotate --arch "${arch}" "${IMAGE_TAG_LATEST}" "${IMAGE_TAG}-${arch}" + done + + # Push virual tag metadata. + docker manifest push "${IMAGE_TAG_LATEST}" +fi diff --git a/scripts/build/docker/publish-image.sh b/scripts/build/docker/publish-image.sh new file mode 100755 index 0000000..fef0c4f --- /dev/null +++ b/scripts/build/docker/publish-image.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh + +set -e + + +[ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1; +[ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1; + +DEF_ARCH=amd64 +ARCH=${ARCH:-$DEF_ARCH} + +IMAGE_TAG_ARCH="${IMAGE}:${VERSION}-${ARCH}" + +echo "Pushing image ${IMAGE_TAG_ARCH}..." +docker push ${IMAGE_TAG_ARCH} diff --git a/scripts/check/check.sh b/scripts/check/check.sh new file mode 100755 index 0000000..824e59c --- /dev/null +++ b/scripts/check/check.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +golangci-lint run \ No newline at end of file diff --git a/scripts/check/integration-test.sh b/scripts/check/integration-test.sh new file mode 100755 index 0000000..04aeec2 --- /dev/null +++ b/scripts/check/integration-test.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go test -race -tags='integration' -v ./test/integration/... \ No newline at end of file diff --git a/scripts/check/unit-test.sh b/scripts/check/unit-test.sh new file mode 100755 index 0000000..b0dfc8b --- /dev/null +++ b/scripts/check/unit-test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go test -race -coverprofile=.test_coverage.txt $(go list ./... | grep -v /test/integration ) +go tool cover -func=.test_coverage.txt | tail -n1 | awk '{print "Total test coverage: " $3}' \ No newline at end of file diff --git a/scripts/deps.sh b/scripts/deps.sh new file mode 100755 index 0000000..f112039 --- /dev/null +++ b/scripts/deps.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go mod tidy \ No newline at end of file diff --git a/scripts/gogen.sh b/scripts/gogen.sh new file mode 100755 index 0000000..803ac8a --- /dev/null +++ b/scripts/gogen.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh + +set -o errexit +set -o nounset + +go generate ./... \ No newline at end of file diff --git a/scripts/publish-image.sh b/scripts/publish-image.sh deleted file mode 100755 index d705478..0000000 --- a/scripts/publish-image.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env sh - -set -e - -[ -z "$VERSION" ] && echo "VERSION env var is required." && exit 1 -[ -z "$IMAGE" ] && echo "IMAGE env var is required." && exit 1 - -IMAGE_TAG="${IMAGE}:${VERSION}" - -echo "Pushing image ${IMAGE_TAG}..." -docker push ${IMAGE_TAG}