Skip to content

Commit 9d8ef22

Browse files
GabriFedi97NiccoloFeigbartolini
authored
feat: automate extensions local testing (#75)
Add Dagger and Taskfile implementation to run Chainsaw E2E tests for extension images. Closes #14 Signed-off-by: Gabriele Fedi <[email protected]> Signed-off-by: Niccolò Fei <[email protected]> Signed-off-by: Gabriele Bartolini <[email protected]> Co-authored-by: Niccolò Fei <[email protected]> Co-authored-by: Gabriele Bartolini <[email protected]>
1 parent 69ec927 commit 9d8ef22

File tree

7 files changed

+272
-34
lines changed

7 files changed

+272
-34
lines changed

.github/workflows/bake_targets.yml

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -138,33 +138,17 @@ jobs:
138138
- name: Set up environment
139139
run: |
140140
task e2e:setup-env
141-
task e2e:export-kubeconfig
142141
143142
- name: Generate Chainsaw testing values
144-
uses: dagger/dagger-for-github@d913e70051faf3b907d4dd96ef1161083c88c644 # v8.2.0
145-
env:
146-
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
147-
DAGGER_VERSION: 0.19.8
148-
with:
149-
version: ${{ env.DAGGER_VERSION }}
150-
verb: call
151-
module: ./dagger/maintenance/
152-
args: generate-testing-values --target ${{ inputs.extension_name }} --extension-image ${{ matrix.image }} export --path=${{ inputs.extension_name }}/values.yaml
153-
154-
- name: Install Chainsaw
155-
uses: kyverno/action-install-chainsaw@06560d18422209e9c1e08e931d477d04bf2674c1 # v0.2.14
143+
run: |
144+
task e2e:generate-values EXTENSION_IMAGE="${{ matrix.image }}" TARGET="${{ inputs.extension_name }}"
156145
157-
- name: Run Kyverno/Chainsaw
158-
env:
159-
EXT_NAME: ${{ inputs.extension_name }}
146+
- name: Run e2e tests
160147
run: |
161-
# Common smoke tests
162-
chainsaw test ./test --values "$EXT_NAME/values.yaml"
148+
# Get Kind cluster internal kubeconfig
149+
task e2e:export-kubeconfig KUBECONFIG_PATH=./kubeconfig INTERNAL=true
163150
164-
# Specific smoke tests
165-
if [ -d "$EXT_NAME/test" ]; then
166-
chainsaw test "$EXT_NAME/test" --values "$EXT_NAME/values.yaml"
167-
fi
151+
task e2e:test TARGET="${{ inputs.extension_name }}" KUBECONFIG_PATH="./kubeconfig"
168152
169153
copytoproduction:
170154
name: Copy images to production

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@
2828

2929
# Go workspace file
3030
go.work
31+
32+
# Chainsaw values files
33+
**/values.yaml

BUILD.md

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,26 +84,105 @@ task DRY_RUN=true
8484
task bake TARGET=pgvector DRY_RUN=true
8585
```
8686

87-
## Testing locally
87+
## Local testing guide
8888

89-
Local testing can be performed by using a local Docker container registry and a Kind cluster with CNPG installed.
90-
The Taskfile includes utilities to set up and tear down such an environment.
89+
Testing your extensions locally ensures high-quality PRs and faster iteration
90+
cycles. This environment uses a local Docker container registry and a Kind
91+
cluster with CloudNativePG pre-installed.
9192

92-
### Create a local test environment
93+
> [!IMPORTANT]
94+
> **Pre-submission requirement:** You must successfully run local tests before
95+
> submitting a Pull Request for any extension.
9396
94-
The `e2e:setup-env` task takes care of setting up a Kind cluster with a local Docker container registry connected to the same
95-
Docker network and installs CloudNativePG by default.
97+
### Initialize the environment
98+
99+
The `e2e:setup-env` utility automates the heavy lifting. It creates a Kind
100+
cluster, attaches a local Docker registry (available at `localhost:5000`), and
101+
installs the CloudNativePG operator.
96102

97103
```bash
98104
task e2e:setup-env
99105
```
100106

101-
The container registry will be exposed locally at `localhost:5000`.
107+
### Get access to the cluster
108+
109+
Even though the cluster is running, your local `kubectl` doesn't know how to
110+
talk to it yet. You need to "export" the credentials (the Kubeconfig).
111+
112+
If you want to run `kubectl get pods` from your own laptop's terminal, use the
113+
standard export:
114+
115+
```bash
116+
task e2e:export-kubeconfig KUBECONFIG_PATH=./kubeconfig
117+
export KUBECONFIG=$PWD/kubeconfig
118+
```
119+
120+
If you are running a test script that is also running inside a Docker container
121+
on the same network, like in the case of Kind, it needs the "internal" address
122+
to find the API server:
123+
124+
```bash
125+
task e2e:export-kubeconfig KUBECONFIG_PATH=./kubeconfig INTERNAL=true
126+
```
127+
128+
### Build and push the extension (`bake`)
129+
130+
Before the cluster can use your extension, you must build the image and push it
131+
to the local registry (see ["Push images for a specific project" above](#6-push-images-for-a-specific-project)):
132+
133+
```bash
134+
task bake TARGET="<extension>" PUSH=true
135+
```
136+
137+
This command tags the image for `localhost:5000` and pushes it automatically.
138+
139+
> [!TIP]
140+
> You can change the default registry through the `registry` environment variable
141+
> (defined in the `docker/bake.hcl` file).
142+
143+
### Prepare testing values
144+
145+
We use [Chainsaw](https://github.com/kyverno/chainsaw) for declarative
146+
end-to-end testing. Before running tests, you must generate specific
147+
configuration values for your extension image.
148+
149+
Run the following command to export these values into your extension's
150+
directory:
151+
152+
```bash
153+
task e2e:generate-values TARGET="<extension>" EXTENSION_IMAGE="<my-local-image>"
154+
```
155+
156+
For example, to generate the values for the local test of the local image, you could run something similar to the following:
157+
158+
```bash
159+
# The actual name of the image might be different on your system
160+
task e2e:generate-values TARGET=pgvector EXTENSION_IMAGE="localhost:5000/pgvector-testing:0.8.1-18-trixie"
161+
```
162+
163+
### Execute End-to-End tests
164+
165+
The testing framework requires an internal Kubeconfig to communicate correctly
166+
within the Docker network.
167+
168+
First, export the internal configuration as shown above:
169+
170+
```bash
171+
task e2e:export-kubeconfig KUBECONFIG_PATH=./kubeconfig INTERNAL=true
172+
```
173+
174+
Then, run the `e2e:test` task. This executes both the generic tests (located in
175+
the global `/test` folder) and any extension-specific tests (located in the
176+
target's `/test` folder):
177+
178+
```bash
179+
task e2e:test TARGET="<extension>" KUBECONFIG_PATH="./kubeconfig"
180+
```
102181

103-
The Kubeconfig to connect to the Kind cluster can be retrieved with:
182+
You can test the `pgvector` extension with:
104183

105184
```bash
106-
task e2e:export-kubeconfig KUBECONFIG_PATH=<path-to-export-kubeconfig>
185+
task e2e:test TARGET="pgvector" KUBECONFIG_PATH="./kubeconfig"
107186
```
108187

109188
### Tear down the local test environment

Taskfile.yml

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,24 @@ tasks:
128128
TARGET: "{{.ITEM}}"
129129
task: update-os-libs
130130

131+
generate-values:
132+
desc: Generate Chainsaw testing values for the specified target
133+
deps:
134+
- prereqs
135+
prefix: 'generate-values-{{.TARGET}}'
136+
vars:
137+
EXTENSION_IMAGE: '{{ .EXTENSION_IMAGE| default "" }}'
138+
env:
139+
_EXPERIMENTAL_DAGGER_RUNNER_HOST: '{{ ._EXPERIMENTAL_DAGGER_RUNNER_HOST | default "" }}'
140+
cmds:
141+
- echo -e "{{.BLUE}}Generating values for target {{.TARGET}}...{{.NC}}"
142+
- >
143+
dagger call -sm ./dagger/maintenance/ generate-testing-values
144+
--target {{ .TARGET }} --extension-image="{{ .EXTENSION_IMAGE }}" export --path {{.TARGET}}/values.yaml
145+
requires:
146+
vars:
147+
- name: TARGET
148+
131149
e2e:create-docker-network:
132150
desc: Create Docker network to connect all the services, such as the Registry, Kind nodes, Chainsaw, etc.
133151
run: once
@@ -177,7 +195,7 @@ tasks:
177195
internal: true
178196
run: once
179197
vars:
180-
REGISTRY_DIR: /etc/containerd/certs.d/localhost:{{ .REGISTRY_HOST_PORT }}
198+
REGISTRY_DIR: /etc/containerd/certs.d/{{ .REGISTRY_NAME }}:{{ .REGISTRY_HOST_PORT }}
181199
DOCKER_SOCKET:
182200
sh: docker context inspect -f {{`'{{json .Endpoints.docker.Host}}'`}} $(docker context show)
183201
env:
@@ -254,6 +272,7 @@ tasks:
254272
- e2e:setup-kind
255273
vars:
256274
KUBECONFIG_PATH: '{{.KUBECONFIG_PATH| default "~/.kube/config"}}'
275+
INTERNAL: '{{.INTERNAL| default "false"}}'
257276
DOCKER_SOCKET:
258277
sh: docker context inspect -f {{`'{{json .Endpoints.docker.Host}}'`}} $(docker context show)
259278
env:
@@ -262,7 +281,7 @@ tasks:
262281
- |
263282
CONFIG=$(dagger call -m github.com/aweris/daggerverse/kind@{{ .DAGGER_KIND_SHA }} \
264283
--socket {{ .DOCKER_SOCKET }} container \
265-
with-exec --args "kind","get","kubeconfig","--name","{{ .KIND_CLUSTER_NAME }}" stdout)
284+
with-exec --args "kind","get","kubeconfig","--name","{{ .KIND_CLUSTER_NAME }}","--internal={{ .INTERNAL }}" stdout)
266285
mkdir -p $(dirname {{ .KUBECONFIG_PATH }})
267286
echo "${CONFIG}" > {{ .KUBECONFIG_PATH }}
268287
@@ -276,6 +295,54 @@ tasks:
276295
cmds:
277296
- echo -e "{{.GREEN}}--- E2E environment setup complete ---{{.NC}}"
278297

298+
e2e:generate-values:
299+
desc: Generate Chainsaw testing values for the specified target in e2e environment
300+
deps:
301+
- e2e:start-dagger-engine
302+
- e2e:start-container-registry
303+
prefix: 'e2e:generate-values-{{ .TARGET }}'
304+
silent: true
305+
vars:
306+
EXTENSION_IMAGE: '{{ .EXTENSION_IMAGE| default "" }}'
307+
cmds:
308+
- task: generate-values
309+
vars:
310+
_EXPERIMENTAL_DAGGER_RUNNER_HOST: container://{{ .DAGGER_ENGINE_NAME }}
311+
TARGET: '{{ .TARGET }}'
312+
EXTENSION_IMAGE:
313+
# We need to replace localhost with the registry container name as it is how
314+
# the registry is reachable from within the Docker network.
315+
sh: sed -E 's/^localhost/{{ .REGISTRY_NAME }}/;t' <<< "{{ .EXTENSION_IMAGE }}"
316+
317+
e2e:test:
318+
desc: Test target extension using Chainsaw
319+
deps:
320+
- e2e:start-dagger-engine
321+
prefix: 'e2e:test-{{ .TARGET }}'
322+
silent: true
323+
vars:
324+
KUBECONFIG_PATH: '{{ .KUBECONFIG_PATH | default "~/.kube/config" }}'
325+
env:
326+
_EXPERIMENTAL_DAGGER_RUNNER_HOST: container://{{ .DAGGER_ENGINE_NAME }}
327+
cmds:
328+
- echo -e "{{ .BLUE }}Testing {{ .TARGET }}...{{ .NC }}"
329+
- dagger call -m ./dagger/maintenance/ test --source . --target {{ .TARGET }} --kubeconfig {{ .KUBECONFIG_PATH }}
330+
requires:
331+
vars:
332+
- name: TARGET
333+
334+
e2e:test:all:
335+
desc: Test all the available targets using Chainsaw
336+
vars:
337+
TARGETS:
338+
sh: dagger call -sm ./dagger/maintenance/ get-targets | tr -d '[]"' | tr ',' '\n'
339+
cmds:
340+
- for:
341+
var: TARGETS
342+
vars:
343+
TARGET: "{{ .ITEM }}"
344+
task: e2e:test
345+
279346
e2e:cleanup:
280347
desc: Cleanup E2E resources
281348
deps:

dagger/maintenance/image.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const (
1919

2020
// getImageAnnotations returns the OCI annotations given an image ref.
2121
func getImageAnnotations(imageRef string) (map[string]string, error) {
22-
ref, err := name.ParseReference(imageRef)
22+
// Setting Insecure option to allow fetching images from local registries with no TLS
23+
ref, err := name.ParseReference(imageRef, name.Insecure)
2324
if err != nil {
2425
return nil, err
2526
}

dagger/maintenance/main.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"maps"
1111
"path"
1212
"slices"
13+
"time"
1314

1415
"go.yaml.in/yaml/v3"
1516

@@ -189,3 +190,96 @@ func (m *Maintenance) GenerateTestingValues(
189190

190191
return result.File("values.yaml"), nil
191192
}
193+
194+
// Tests the specified target using Chainsaw
195+
func (m *Maintenance) Test(
196+
ctx context.Context,
197+
// The source directory containing the extension folders. Defaults to the current directory
198+
// +ignore=["dagger", ".github"]
199+
// +defaultPath="/"
200+
source *dagger.Directory,
201+
// Kubeconfig to connect to the target K8s
202+
// +required
203+
kubeconfig *dagger.File,
204+
// The target extension to test
205+
// +default="all"
206+
target string,
207+
// Container image to use to run chainsaw
208+
// renovate: datasource=docker depName=kyverno/chainsaw packageName=ghcr.io/kyverno/chainsaw versioning=docker
209+
// +default="ghcr.io/kyverno/chainsaw:v0.2.14@sha256:c703e4d4ce7b89c5121fe957ab89b6e2d33f91fd15f8274a9f79ca1b2ba8ecef"
210+
chainsawImage string,
211+
) error {
212+
extDir := source
213+
if target != "all" {
214+
extDir = source.Filter(dagger.DirectoryFilterOpts{
215+
Include: []string{path.Join(target, "**"), "test"},
216+
})
217+
hasMetadataFile, err := extDir.Exists(ctx, path.Join(target, metadataFile))
218+
if err != nil {
219+
return err
220+
}
221+
if !hasMetadataFile {
222+
return fmt.Errorf("not a valid target, metadata.hcl file is missing. Target: %s", target)
223+
}
224+
}
225+
226+
targetExtensions, err := extensionsDirectories(ctx, extDir)
227+
if err != nil {
228+
return err
229+
}
230+
231+
const valuesFile = "values.yaml"
232+
233+
for _, targetExtension := range targetExtensions {
234+
extName, err := targetExtension.Name(ctx)
235+
if err != nil {
236+
return err
237+
}
238+
239+
hasValues, err := targetExtension.Exists(ctx, valuesFile)
240+
if err != nil {
241+
return err
242+
}
243+
if !hasValues {
244+
return fmt.Errorf("cannot execute tests for extension %q, values.yaml file is missing", target)
245+
}
246+
247+
ctr := dag.Container().From(chainsawImage).
248+
WithWorkdir("e2e").
249+
WithEnvVariable("CACHEBUSTER", time.Now().String()).
250+
WithDirectory("test", extDir.Directory("test")).
251+
WithDirectory(extName, targetExtension).
252+
WithFile("/etc/kubeconfig/config", kubeconfig).
253+
WithEnvVariable("KUBECONFIG", "/etc/kubeconfig/config")
254+
255+
_, err = ctr.WithExec(
256+
[]string{"test", "./test", "--values", path.Join(extName, valuesFile)},
257+
dagger.ContainerWithExecOpts{
258+
UseEntrypoint: true,
259+
}).
260+
Sync(ctx)
261+
262+
if err != nil {
263+
return err
264+
}
265+
266+
hasIndividualTests, err := targetExtension.Exists(ctx, "test")
267+
if err != nil {
268+
return err
269+
}
270+
if !hasIndividualTests {
271+
continue
272+
}
273+
_, err = ctr.WithExec(
274+
[]string{"test", path.Join(extName, "test"), "--values", path.Join(extName, valuesFile)},
275+
dagger.ContainerWithExecOpts{
276+
UseEntrypoint: true,
277+
}).
278+
Sync(ctx)
279+
if err != nil {
280+
return err
281+
}
282+
}
283+
284+
return nil
285+
}

renovate.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@
4646
"# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: currentValue=(?<currentValue>[^\\s]+?))?\\s+[A-Za-z0-9_]+?_SHA\\s*:\\s*[\"']?(?<currentDigest>[a-f0-9]+?)[\"']?\\s",
4747
"# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?\\s+[^:]+\\s*:\\s*[\"']?(?<currentValue>[^@]+?)(@(?<currentDigest>sha256:[0-9a-f]+))?[\"']?\\s"
4848
]
49+
},
50+
{
51+
"description": "updates the dagger dependencies",
52+
"customType": "regex",
53+
"managerFilePatterns": [
54+
"dagger/**/*.go"
55+
],
56+
"matchStrings": [
57+
"\\/\\/\\s+renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))\\s+\\/\\/\\s+\\+default=[\\\"']?[^:]+?:(?<currentValue>[^@]+?)(@(?<currentDigest>sha256:[0-9a-f]+))?[\"']?\\s"
58+
]
4959
}
5060
],
5161
"packageRules": [

0 commit comments

Comments
 (0)