From ba9854ec32c970f0246cbfec713bd94c6194b434 Mon Sep 17 00:00:00 2001 From: Francesco Canovai Date: Thu, 19 Dec 2024 16:20:17 +0100 Subject: [PATCH] test(e2e): run on ephemeral clusters Rework the e2e test to expect a working connection to a cluster when they start. Developers can create their own clusters and run the tests. Removed the code used to start kind clusters within the e2e tests. Reworked the Taskfile to define two environments where the tests can run: 1. An ephemeral one running within Dagger, using the k3s module, to be used by the CI. 2. A persistent one created with Kind, requiring the kind binary, to be used for development and debugging when the ephemeral cluster is not enough. Signed-off-by: Francesco Canovai --- .github/workflows/ci.yml | 5 - Taskfile.yml | 126 +++++++++++----- dagger/e2e/.gitattributes | 4 + dagger/e2e/.gitignore | 4 + dagger/e2e/dagger.json | 18 +++ dagger/e2e/go.mod | 51 +++++++ dagger/e2e/go.sum | 85 +++++++++++ dagger/e2e/main.go | 98 ++++++++++++ {test/e2e/config => hack}/kind-config.yaml | 2 +- test/e2e/e2e_suite_test.go | 12 +- test/e2e/internal/e2etestenv/main.go | 95 +----------- test/e2e/internal/kind/cluster.go | 164 --------------------- test/e2e/internal/kind/doc.go | 19 --- 13 files changed, 366 insertions(+), 317 deletions(-) create mode 100644 dagger/e2e/.gitattributes create mode 100644 dagger/e2e/.gitignore create mode 100644 dagger/e2e/dagger.json create mode 100644 dagger/e2e/go.mod create mode 100644 dagger/e2e/go.sum create mode 100644 dagger/e2e/main.go rename {test/e2e/config => hack}/kind-config.yaml (90%) delete mode 100644 test/e2e/internal/kind/cluster.go delete mode 100644 test/e2e/internal/kind/doc.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f78af7..963f485 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,11 +32,6 @@ jobs: with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - # TODO: remove this when we daggerize the e2e - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version: '1.23.x' - name: Install Task uses: arduino/setup-task@v2 - name: Install Dagger diff --git a/Taskfile.yml b/Taskfile.yml index d1e5a9a..93e9543 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,7 +1,14 @@ version: 3 -# Environment variables that are shared across tasks. -env: +# We have multiple parallel tasks that run for a long time. Prefix their output with the task name so we can understand +# what task is writing. +output: prefixed + +# Variables that are shared across tasks. +vars: + # renovate: datasource=docker depName=kindest/node versioning=semver + E2E_KUBERNETES_VERSION: v1.32.0 + E2E_CLUSTER_NAME: barman-cloud-plugin-e2e-{{.E2E_KUBERNETES_VERSION}} REGISTRY_NETWORK: barman-cloud-plugin REGISTRY_NAME: registry.barman-cloud-plugin REGISTRY_PORT: 5000 @@ -89,8 +96,8 @@ tasks: openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem \ -subj "/O=CloudNativePG/OU=Barman Cloud Plugin Testing" && openssl genrsa -out server-key.pem 4096 && - openssl req -subj "/CN=${REGISTRY_NAME}" -sha256 -new -key server-key.pem -out server.csr && - echo subjectAltName = DNS:${REGISTRY_NAME},IP:127.0.0.1 >> extfile.cnf && + openssl req -subj "/CN={{ .REGISTRY_NAME }}" -sha256 -new -key server-key.pem -out server.csr && + echo subjectAltName = DNS:{{ .REGISTRY_NAME }},IP:127.0.0.1 >> extfile.cnf && echo extendedKeyUsage = serverAuth >> extfile.cnf && openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \ -CAcreateserial -out server-cert.pem -extfile extfile.cnf && @@ -106,9 +113,9 @@ tasks: desc: Create a docker network for image building used by the dagger engine and the registry run: once cmds: - - docker network create ${REGISTRY_NETWORK} + - docker network create {{ .REGISTRY_NETWORK}} status: - - docker network inspect ${REGISTRY_NETWORK} + - docker network inspect {{ .REGISTRY_NETWORK }} start-registry: desc: Start a container registry @@ -121,14 +128,14 @@ tasks: REGISTRY_VERSION: 2 cmds: - > - docker run -d --name ${REGISTRY_NAME} - -p ${REGISTRY_PORT}:5000 - --network ${REGISTRY_NETWORK} + docker run -d --name {{ .REGISTRY_NAME }} + -p {{ .REGISTRY_PORT }}:5000 + --network {{ .REGISTRY_NETWORK }} -v $(pwd)/certs:/certs -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server-cert.pem -e REGISTRY_HTTP_TLS_KEY=/certs/server-key.pem registry:${REGISTRY_VERSION} status: - - \[ "$(docker inspect -f {{`'{{.State.Running}}'`}} "${REGISTRY_NAME}" 2> /dev/null )" == 'true' \] + - \[ "$(docker inspect -f {{`'{{.State.Running}}'`}} "{{ .REGISTRY_NAME }}" 2> /dev/null )" == 'true' \] # Start a dagger engine that mounts the CA certificate for the local registry. @@ -144,12 +151,12 @@ tasks: DAGGER_ENGINE_IMAGE: registry.dagger.io/engine:v{{ .DAGGER_VERSION }} cmds: - > - docker run -d -v /var/lib/dagger --name "${DAGGER_ENGINE_CONTAINER_NAME}" - --network=${REGISTRY_NETWORK} + docker run -d -v /var/lib/dagger --name "{{ .DAGGER_ENGINE_CONTAINER_NAME }}" + --network={{ .REGISTRY_NETWORK }}} -v $(pwd)/certs/ca.pem:/usr/local/share/ca-certificates/ca.crt --privileged {{ .DAGGER_ENGINE_IMAGE }} status: - - \[ "$(docker inspect -f {{`'{{.State.Running}}'`}} "${DAGGER_ENGINE_CONTAINER_NAME}" 2> /dev/null )" == 'true' \] + - \[ "$(docker inspect -f {{`'{{.State.Running}}'`}} "{{ .DAGGER_ENGINE_CONTAINER_NAME }}" 2> /dev/null )" == 'true' \] # We build an image and push it to a local registry. # The name is always `plugin-barman-cloud:testing`. @@ -161,12 +168,12 @@ tasks: env: # renovate: datasource=git-refs depName=docker lookupName=https://github.com/purpleclay/daggerverse currentValue=main DAGGER_DOCKER_SHA: 14c1374e5878f082939aab575c36cdad19920e0d - _EXPERIMENTAL_DAGGER_RUNNER_HOST: docker-container://{{.DAGGER_ENGINE_CONTAINER_NAME}} + _EXPERIMENTAL_DAGGER_RUNNER_HOST: docker-container://{{ .DAGGER_ENGINE_CONTAINER_NAME }} cmds: - > - GITHUB_REF= dagger call -m github.com/purpleclay/daggerverse/docker@${DAGGER_DOCKER_SHA} + GITHUB_REF= dagger -s call -m github.com/purpleclay/daggerverse/docker@${DAGGER_DOCKER_SHA} build --dir . --file containers/Dockerfile.plugin --platform linux/amd64 - publish --ref ${REGISTRY_NAME}:${REGISTRY_PORT}/plugin-barman-cloud --tags testing + publish --ref {{ .REGISTRY_NAME }}:{{ .REGISTRY_PORT }}/plugin-barman-cloud --tags testing # We build an image and push it to a local registry. # The name is always `sidecar-barman-cloud:testing`. @@ -178,12 +185,12 @@ tasks: env: # renovate: datasource=git-refs depName=docker lookupName=https://github.com/purpleclay/daggerverse currentValue=main DAGGER_DOCKER_SHA: 14c1374e5878f082939aab575c36cdad19920e0d - _EXPERIMENTAL_DAGGER_RUNNER_HOST: docker-container://{{.DAGGER_ENGINE_CONTAINER_NAME}} + _EXPERIMENTAL_DAGGER_RUNNER_HOST: docker-container://{{ .DAGGER_ENGINE_CONTAINER_NAME }} cmds: - > - GITHUB_REF= dagger call -m github.com/purpleclay/daggerverse/docker@${DAGGER_DOCKER_SHA} + GITHUB_REF= dagger -s call -m github.com/purpleclay/daggerverse/docker@${DAGGER_DOCKER_SHA} build --dir . --file containers/Dockerfile.sidecar --platform linux/amd64 - publish --ref ${REGISTRY_NAME}:${REGISTRY_PORT}/sidecar-barman-cloud --tags testing + publish --ref {{ .REGISTRY_NAME }}:{{ .REGISTRY_PORT }}/sidecar-barman-cloud --tags testing build-images: desc: Build the container images for the plugin @@ -191,6 +198,39 @@ tasks: - build-plugin-image - build-sidecar-image + # Install kind if not at the expected version. + install-kind: + desc: Install kind + run: once + vars: + # renovate: datasource=git-refs depName=kind lookupName=https://github.com/kubernetes-sigs/kind versioning=semver + KIND_VERSION: v0.26.0 + cmds: + - go install sigs.k8s.io/kind@{{.KIND_VERSION}} + - kind version | grep -q {{.KIND_VERSION}} + status: + - kind version | grep -q {{.KIND_VERSION}} + + start-kind-cluster: + desc: Start a kind cluster + deps: + - install-kind + - start-build-network + run: once + cmds: + - > + kind create cluster --name {{ .E2E_CLUSTER_NAME }} + --image kindest/node:{{ .E2E_KUBERNETES_VERSION }} + --config hack/kind-config.yaml + --wait 5m + - > + for node in $(kind get nodes --name {{ .E2E_CLUSTER_NAME }} ); do + docker network connect {{ .REGISTRY_NETWORK }}} $node; + docker exec $node sh -c "update-ca-certificates"; + done + status: + - kind get clusters | grep -q {{ .E2E_CLUSTER_NAME }} + # TODO: see if it is possible to daggerize this. It will have to manage docker to make kind work. # TODO: add a task to clean up the kind cluster for new test runs. # Run the e2e tests. This task will start a kind cluster, deploy the plugin, and run the tests. @@ -199,22 +239,42 @@ tasks: # * The registry to be in the same network of the dagger-engine. # * The dagger-engine to mount the CA. # * The kind cluster to mount the CA. - e2e: - desc: Run e2e tests + e2e-external-kind: + desc: Run e2e tests in a local kind cluster deps: - build-images + - start-kind-cluster + vars: + # renovate: datasource=docker depName=golang versioning=semver + GOLANG_IMAGE_VERSION: 1.23.4 + KUBECONFIG_PATH: + sh: mktemp -t kubeconfig-XXXXX + env: + _EXPERIMENTAL_DAGGER_RUNNER_HOST: docker-container://{{ .DAGGER_ENGINE_CONTAINER_NAME }} + cmds: + - kind get kubeconfig --internal --name {{ .E2E_CLUSTER_NAME }} > {{ .KUBECONFIG_PATH }} + - > + GITHUB_REF= dagger call -m dagger/e2e/ run + --source . + --kubeconfig {{.KUBECONFIG_PATH}} + --go-version {{ .GOLANG_IMAGE_VERSION }} + + e2e-ephemeral: + desc: Run e2e tests in an ephemeral k3s cluster + deps: + - build-images + vars: + # renovate: datasource=docker depName=golang versioning=semver + GOLANG_IMAGE_VERSION: 1.23.4 + env: + _EXPERIMENTAL_DAGGER_RUNNER_HOST: docker-container://{{ .DAGGER_ENGINE_CONTAINER_NAME }} cmds: - > - go run github.com/onsi/ginkgo/v2/ginkgo - --procs=8 - --randomize-all - --randomize-suites - --fail-on-pending - --fail-on-empty - --keep-going - --timeout=30m - --github-output - ./test/e2e + GITHUB_REF= dagger call -m dagger/e2e/ run-ephemeral + --source . + --ca certs/ca.pem + --registry {{.REGISTRY_NAME}}:{{.REGISTRY_PORT}} + --go-version {{ .GOLANG_IMAGE_VERSION }} ci: desc: Run the CI pipeline @@ -224,7 +284,7 @@ tasks: - uncommitted - lint - go-test - - e2e + - e2e-ephemeral publish: desc: Build and publish a container image for the plugin @@ -284,7 +344,7 @@ tasks: - controller-gen desc: Generate the manifest for the main branch vars: - GITHUB_REPOSITORY: '{{ default "cloudnative-pg/plugin-barman-cloud" .GITHUB_REPOSITORY }}' + GITHUB_REPOSITORY: cloudnative-pg/plugin-barman-cloud GITHUB_REF: main GITHUB_REF_NAME: main cmds: diff --git a/dagger/e2e/.gitattributes b/dagger/e2e/.gitattributes new file mode 100644 index 0000000..3a45493 --- /dev/null +++ b/dagger/e2e/.gitattributes @@ -0,0 +1,4 @@ +/dagger.gen.go linguist-generated +/internal/dagger/** linguist-generated +/internal/querybuilder/** linguist-generated +/internal/telemetry/** linguist-generated diff --git a/dagger/e2e/.gitignore b/dagger/e2e/.gitignore new file mode 100644 index 0000000..7ebabcc --- /dev/null +++ b/dagger/e2e/.gitignore @@ -0,0 +1,4 @@ +/dagger.gen.go +/internal/dagger +/internal/querybuilder +/internal/telemetry diff --git a/dagger/e2e/dagger.json b/dagger/e2e/dagger.json new file mode 100644 index 0000000..ee944fa --- /dev/null +++ b/dagger/e2e/dagger.json @@ -0,0 +1,18 @@ +{ + "name": "e2e", + "engineVersion": "v0.15.1", + "sdk": "go", + "dependencies": [ + { + "name": "go", + "source": "github.com/sagikazarmark/daggerverse/go@go/v0.9.0", + "pin": "d9ba06776c4c1ccf6f329bd862b9b439c4582ab6" + }, + { + "name": "k3s", + "source": "github.com/marcosnils/daggerverse/k3s@k3s/v0.1.7", + "pin": "833ec36632b2457862f6e3bf1f7107ad65e3e515" + } + ], + "source": "." +} diff --git a/dagger/e2e/go.mod b/dagger/e2e/go.mod new file mode 100644 index 0000000..61664d8 --- /dev/null +++ b/dagger/e2e/go.mod @@ -0,0 +1,51 @@ +module dagger/e-2-e + +go 1.23.2 + +require ( + github.com/99designs/gqlgen v0.17.57 + github.com/Khan/genqlient v0.7.0 + github.com/vektah/gqlparser/v2 v2.5.19 + go.opentelemetry.io/otel v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 + go.opentelemetry.io/otel/log v0.3.0 + go.opentelemetry.io/otel/metric v1.27.0 + go.opentelemetry.io/otel/sdk v1.27.0 + go.opentelemetry.io/otel/sdk/log v0.3.0 + go.opentelemetry.io/otel/sdk/metric v1.27.0 + go.opentelemetry.io/otel/trace v1.27.0 + go.opentelemetry.io/proto/otlp v1.3.1 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/sync v0.10.0 + google.golang.org/grpc v1.68.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/sosodev/duration v1.3.1 // indirect + github.com/stretchr/testify v1.10.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect + golang.org/x/net v0.29.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/protobuf v1.35.2 // indirect +) + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 + +replace go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp => go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 + +replace go.opentelemetry.io/otel/log => go.opentelemetry.io/otel/log v0.3.0 + +replace go.opentelemetry.io/otel/sdk/log => go.opentelemetry.io/otel/sdk/log v0.3.0 diff --git a/dagger/e2e/go.sum b/dagger/e2e/go.sum new file mode 100644 index 0000000..e566804 --- /dev/null +++ b/dagger/e2e/go.sum @@ -0,0 +1,85 @@ +github.com/99designs/gqlgen v0.17.57 h1:Ak4p60BRq6QibxY0lEc0JnQhDurfhxA67sp02lMjmPc= +github.com/99designs/gqlgen v0.17.57/go.mod h1:Jx61hzOSTcR4VJy/HFIgXiQ5rJ0Ypw8DxWLjbYDAUw0= +github.com/Khan/genqlient v0.7.0 h1:GZ1meyRnzcDTK48EjqB8t3bcfYvHArCUUvgOwpz1D4w= +github.com/Khan/genqlient v0.7.0/go.mod h1:HNyy3wZvuYwmW3Y7mkoQLZsa/R5n5yIRajS1kPBvSFM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= +github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/vektah/gqlparser/v2 v2.5.19 h1:bhCPCX1D4WWzCDvkPl4+TP1N8/kLrWnp43egplt7iSg= +github.com/vektah/gqlparser/v2 v2.5.19/go.mod h1:y7kvl5bBlDeuWIvLtA9849ncyvx6/lj06RsMrEjVy3U= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88 h1:oM0GTNKGlc5qHctWeIGTVyda4iFFalOzMZ3Ehj5rwB4= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.0.0-20240518090000-14441aefdf88/go.mod h1:JGG8ebaMO5nXOPnvKEl+DiA4MGwFjCbjsxT1WHIEBPY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0 h1:ccBrA8nCY5mM0y5uO7FT0ze4S0TuFcWdDB2FxGMTjkI= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.3.0/go.mod h1:/9pb6634zi2Lk8LYg9Q0X8Ar6jka4dkFOylBLbVQPCE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0 h1:bFgvUr3/O4PHj3VQcFEuYKvRZJX1SJDQ+11JXuSB3/w= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.27.0/go.mod h1:xJntEd2KL6Qdg5lwp97HMLQDVeAhrYxmzFseAMDPQ8I= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0 h1:CIHWikMsN3wO+wq1Tp5VGdVRTcON+DmOJSfDjXypKOc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.27.0/go.mod h1:TNupZ6cxqyFEpLXAZW7On+mLFL0/g0TE3unIYL91xWc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 h1:QY7/0NeRPKlzusf40ZE4t1VlMKbqSNT7cJRYzWuja0s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0/go.mod h1:HVkSiDhTM9BoUJU8qE6j2eSWLLXvi1USXjyd2BXT8PY= +go.opentelemetry.io/otel/log v0.3.0 h1:kJRFkpUFYtny37NQzL386WbznUByZx186DpEMKhEGZs= +go.opentelemetry.io/otel/log v0.3.0/go.mod h1:ziCwqZr9soYDwGNbIL+6kAvQC+ANvjgG367HVcyR/ys= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/log v0.3.0 h1:GEjJ8iftz2l+XO1GF2856r7yYVh74URiF9JMcAacr5U= +go.opentelemetry.io/otel/sdk/log v0.3.0/go.mod h1:BwCxtmux6ACLuys1wlbc0+vGBd+xytjmjajwqqIul2g= +go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= +go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/dagger/e2e/main.go b/dagger/e2e/main.go new file mode 100644 index 0000000..c997d35 --- /dev/null +++ b/dagger/e2e/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "fmt" + + "dagger/e-2-e/internal/dagger" +) + +type E2E struct{} + +// Run runs the E2E tests on a Kubernetes cluster. It returns the output of the tests. +// We expect a kubeconfig file that allows access to the cluster, and optionally +// a service to bind to, if the cluster is not directly exposed to the dagger container running the tests. +func (m *E2E) Run( + ctx context.Context, + // source is the directory containing the source code for the project + source *dagger.Directory, + // kubeconfig is the kubeconfig file to use for the tests + kubeconfig *dagger.File, + // svc is the Kubernetes service to bind to. It will be known as "kubernetes" in the container. + // +optional + svc *dagger.Service, + // version of the golang image to use + // +optional + // +default="latest" + goVersion string, +) (string, error) { + goDag := dag.Go(dagger.GoOpts{Version: goVersion}).WithCgoDisabled().WithSource(source) + if svc != nil { + goDag = goDag.WithServiceBinding("kubernetes", svc) + } + return goDag.Container(). + WithMountedFile("/kubeconfig", kubeconfig). + WithEnvVariable("KUBECONFIG", "/kubeconfig"). + WithExec([]string{"go", "run", "github.com/onsi/ginkgo/v2/ginkgo", + "--procs=8", + "--randomize-all", + "--randomize-suites", + "--fail-on-pending", + "--fail-on-empty", + "--keep-going", + "--timeout=30m", + "--github-output", + "./test/e2e"}).Stdout(ctx) +} + +// RunEphemeral creates a k3s cluster in dagger and then runs the E2E tests on it. +// If a private registry is used, its url and the ca certificate for the registry should be provided. +func (m *E2E) RunEphemeral( + ctx context.Context, + // source is the directory containing the source code for the project + source *dagger.Directory, + // registry is a private registry + // +optional + // +default="registry.barman-cloud-plugin:5000" + registry string, + // ca is the certificate authority for the registry + // +optional + ca *dagger.File, + // name is the name of the ephemeral container + // +optional + // +default="e2e" + name string, + // version of the golang image to use + // +optional + // +default="latest" + goVersion string, +) (string, error) { + k3s := dag.K3S(name) + ctr := k3s.Container() + if ca != nil { + ctr = ctr.WithMountedFile("/usr/local/share/ca-certificates/ca.crt", ca) + } + if registry != "" { + ctr = ctr.WithNewFile("/registries.yaml", fmt.Sprintf(` +configs: + "%s": + tls: + ca_file: "/usr/local/share/ca-certificates/ca.crt" +`, registry)). + WithExec([]string{"sh", "-c", "cat /registries.yaml > /etc/rancher/k3s/registries.yaml"}) + } + + ctr, err := ctr.Sync(ctx) + if err != nil { + return "", err + } + kServer := k3s.WithContainer(ctr).Server() + + kServer, err = kServer.Start(ctx) + if err != nil { + return "", err + } + defer kServer.Stop(ctx) + + return m.Run(ctx, source, k3s.Config(), kServer, goVersion) +} diff --git a/test/e2e/config/kind-config.yaml b/hack/kind-config.yaml similarity index 90% rename from test/e2e/config/kind-config.yaml rename to hack/kind-config.yaml index f755bdd..ec055f7 100644 --- a/test/e2e/config/kind-config.yaml +++ b/hack/kind-config.yaml @@ -6,6 +6,6 @@ apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane extraMounts: - - hostPath: ../../certs/ca.pem + - hostPath: certs/ca.pem containerPath: /usr/local/share/ca-certificates/ca.crt readOnly: true diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 8c99052..1922afa 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -24,10 +24,10 @@ import ( apimachineryTypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/controller-runtime/pkg/client" kustomizeTypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/resid" + internalClient "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/client" "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/deployment" "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/e2etestenv" "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/kustomize" @@ -41,10 +41,12 @@ import ( // We don't want multiple ginkgo nodes to run the setup concurrently, we use a single cluster for all tests. var _ = SynchronizedBeforeSuite(func(ctx SpecContext) []byte { - var cl client.Client - var err error - if cl, err = e2etestenv.Setup(ctx, - e2etestenv.WithKindAdditionalNetworks([]string{"barman-cloud-plugin"})); err != nil { + cl, _, err := internalClient.NewClient() + if err != nil { + Fail(fmt.Sprintf("failed to create Kubernetes client: %v", err)) + } + + if err = e2etestenv.Setup(ctx, cl); err != nil { Fail(fmt.Sprintf("failed to setup environment: %v", err)) } diff --git a/test/e2e/internal/e2etestenv/main.go b/test/e2e/internal/e2etestenv/main.go index 089da81..e7aad8d 100644 --- a/test/e2e/internal/e2etestenv/main.go +++ b/test/e2e/internal/e2etestenv/main.go @@ -19,26 +19,16 @@ package e2etestenv import ( "context" "fmt" - "strings" "time" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/kind/pkg/cluster" "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/certmanager" - internalClient "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/client" "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/cloudnativepg" - "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/kind" ) // SetupOptions contains the options for setting up the test environment. type SetupOptions struct { - K8sVersion string - - KindVersion string - KindClusterNamePrefix string - KindAdditionalNetworks []string - CNPGKustomizationURL string CNPGKustomizationRef string CNPGKustomizationTimeout string @@ -53,27 +43,6 @@ type SetupOptions struct { // SetupOption is a function that sets up an option for the test environment setup. type SetupOption func(*SetupOptions) -// WithK8sVersion sets the Kubernetes version for the test environment. -func WithK8sVersion(version string) SetupOption { - return func(opts *SetupOptions) { - opts.K8sVersion = version - } -} - -// WithKindVersion sets the Kind version for the test environment. -func WithKindVersion(version string) SetupOption { - return func(opts *SetupOptions) { - opts.KindVersion = version - } -} - -// WithKindAdditionalNetworks sets the additional networks for the Kind cluster for the test environment. -func WithKindAdditionalNetworks(networks []string) SetupOption { - return func(opts *SetupOptions) { - opts.KindAdditionalNetworks = networks - } -} - // WithCNPGKustomizationURL sets the CloudNativePG kustomization URL for the test environment. func WithCNPGKustomizationURL(url string) SetupOption { return func(opts *SetupOptions) { @@ -124,56 +93,31 @@ func WithIgnoreExistingResources(ignore bool) SetupOption { } } -// WithKindClusterNamePrefix sets the prefix for the Kind cluster name for the test environment. -func withKindClusterNamePrefix(name string) SetupOption { - return func(opts *SetupOptions) { - opts.KindClusterNamePrefix = name - } -} - -const ( - kindConfigFile = "config/kind-config.yaml" -) - func defaultSetupOptions() SetupOptions { // TODO: renovate return SetupOptions{ - K8sVersion: "v1.31.1", - KindVersion: "v0.24.0", - CertManagerVersion: "v1.15.1", - KindClusterNamePrefix: "e2e", - KindAdditionalNetworks: []string{}, + CertManagerVersion: "v1.15.1", } } // Setup sets up the test environment for the e2e tests, starting kind and installing the necessary components. // //nolint:ireturn -func Setup(ctx context.Context, opts ...SetupOption) (client.Client, error) { +func Setup(ctx context.Context, cl client.Client, opts ...SetupOption) error { options := defaultSetupOptions() for _, opt := range opts { opt(&options) } - if err := setupKind(ctx, options); err != nil { - return nil, err - } - - cl, _, err := internalClient.NewClient() - if err != nil { - return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) - } - if err := installCertManager(ctx, cl, options); err != nil { - return nil, err + return err } if err := installCNPG(ctx, cl, options); err != nil { - return nil, err + return err } - // Return the Kubernetes client used for the tests - return cl, nil + return nil } func installCNPG(ctx context.Context, cl client.Client, options SetupOptions) error { @@ -227,32 +171,3 @@ func installCertManager(ctx context.Context, cl client.Client, options SetupOpti return nil } - -func setupKind(ctx context.Context, options SetupOptions) error { - // This function sets up the environment for the e2e tests - // by creating the cluster and installing the necessary - // components. - expectedClusterName := kindClusterName(options.KindClusterNamePrefix, options.K8sVersion) - provider := cluster.NewProvider() - clusterIsRunning, err := kind.IsClusterRunning(provider, expectedClusterName) - if err != nil { - return fmt.Errorf("failed to check if Kind cluster is running: %w", err) - } - if !clusterIsRunning { - kindOpts := []kind.CreateClusterOption{ - kind.WithK8sVersion(options.K8sVersion), - kind.WithConfigFile(kindConfigFile), - kind.WithNetworks(options.KindAdditionalNetworks), - } - if err := kind.CreateCluster(ctx, provider, expectedClusterName, kindOpts...); err != nil { - return fmt.Errorf("failed to create Kind cluster: %w", err) - } - } - - return nil -} - -func kindClusterName(prefix, k8sVersion string) string { - k8sVersion = strings.ReplaceAll(k8sVersion, ".", "-") - return fmt.Sprintf("%s-%s", prefix, k8sVersion) -} diff --git a/test/e2e/internal/kind/cluster.go b/test/e2e/internal/kind/cluster.go deleted file mode 100644 index 05c6803..0000000 --- a/test/e2e/internal/kind/cluster.go +++ /dev/null @@ -1,164 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kind - -import ( - "context" - "fmt" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/strslice" - "github.com/docker/docker/client" - "sigs.k8s.io/kind/pkg/cluster" - "sigs.k8s.io/kind/pkg/cluster/nodes" -) - -// IsClusterRunning checks if a Kind cluster with the given name is running. -func IsClusterRunning(provider *cluster.Provider, clusterName string) (bool, error) { - clusters, err := provider.List() - if err != nil { - return false, fmt.Errorf("failed to list Kind clusters: %w", err) - } - for _, c := range clusters { - if c == clusterName { - return true, nil - } - } - - return false, nil -} - -// CreateClusterOptions are the options for creating a Kind cluster. -type CreateClusterOptions struct { - ConfigFile string - K8sVersion string - Networks []string -} - -// CreateClusterOption is the option for creating a Kind cluster. -type CreateClusterOption func(*CreateClusterOptions) - -// WithConfigFile sets the config file for creating a Kind cluster. -func WithConfigFile(configFile string) CreateClusterOption { - return func(opts *CreateClusterOptions) { - opts.ConfigFile = configFile - } -} - -// WithK8sVersion sets the Kubernetes version for creating a Kind cluster. -func WithK8sVersion(k8sVersion string) CreateClusterOption { - return func(opts *CreateClusterOptions) { - opts.K8sVersion = k8sVersion - } -} - -// WithNetworks sets the network for creating a Kind cluster. -func WithNetworks(networks []string) CreateClusterOption { - return func(opts *CreateClusterOptions) { - opts.Networks = networks - } -} - -// CreateCluster creates a Kind cluster with the given name. -func CreateCluster(ctx context.Context, provider *cluster.Provider, name string, opts ...CreateClusterOption) error { - options := &CreateClusterOptions{} - for _, opt := range opts { - opt(options) - } - - createOpts := []cluster.CreateOption{ - cluster.CreateWithRetain(true), - cluster.CreateWithDisplayUsage(true), - cluster.CreateWithDisplaySalutation(true), - } - if options.ConfigFile != "" { - createOpts = append(createOpts, cluster.CreateWithConfigFile(options.ConfigFile)) - } - if options.K8sVersion != "" { - createOpts = append(createOpts, cluster.CreateWithNodeImage(fmt.Sprintf("kindest/node:%s", options.K8sVersion))) - } - err := provider.Create(name, createOpts...) - if err != nil { - return fmt.Errorf("kind cluster creation failed: %w", err) - } - - // Initialize Docker client - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return fmt.Errorf("failed to create Docker client: %w", err) - } - - // Since a cluster can mount additional certificates, we need to make sure they are - // usable by the nodes in the cluster. - nodeList, err := getNodes(provider, name) - if err != nil { - return err - } - - if err := updateCACertificates(ctx, cli, nodeList); err != nil { - return fmt.Errorf("failed to update CA certificates: %w", err) - } - - if err := connectNetworks(ctx, cli, nodeList, options.Networks); err != nil { - return fmt.Errorf("failed to connect networks: %w", err) - } - - return nil -} - -func updateCACertificates(ctx context.Context, cli *client.Client, nodes []nodes.Node) error { - for _, node := range nodes { - execConfig := container.ExecOptions{ - Cmd: strslice.StrSlice([]string{"update-ca-certificates"}), - AttachStdout: true, - AttachStderr: true, - } - execID, err := cli.ContainerExecCreate(ctx, node.String(), execConfig) - if err != nil { - return fmt.Errorf("failed to create exec instance in node %s: %w", node.String(), err) - } - - err = cli.ContainerExecStart(ctx, execID.ID, container.ExecStartOptions{}) - if err != nil { - return fmt.Errorf("failed to start exec instance in node %s: %w", node.String(), err) - } - } - - return nil -} - -func connectNetworks(ctx context.Context, cli *client.Client, nodes []nodes.Node, networks []string) error { - for _, netw := range networks { - for _, node := range nodes { - err := cli.NetworkConnect(ctx, netw, node.String(), nil) - if err != nil { - return fmt.Errorf("failed to connect node %s to network %s: %w", node.String(), netw, err) - } - } - } - - return nil -} - -func getNodes(provider *cluster.Provider, clusterName string) ([]nodes.Node, error) { - nodeList, err := provider.ListNodes(clusterName) - if err != nil { - return nil, fmt.Errorf("failed to get Kind nodes: %w", err) - } - - return nodeList, nil -} diff --git a/test/e2e/internal/kind/doc.go b/test/e2e/internal/kind/doc.go deleted file mode 100644 index b770ac5..0000000 --- a/test/e2e/internal/kind/doc.go +++ /dev/null @@ -1,19 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package kind provides utilities for ensuring the presence and correct version of the Kind binary, -// as well as functions for installing and managing the Kind binary in a local project. -package kind