Skip to content

Commit ffebda3

Browse files
committed
ci: separate oci-pull tests from legacy tests
since they're considered integration tests and can't be run inside a docker container due to testcontainers-go
1 parent 30a8225 commit ffebda3

File tree

4 files changed

+205
-183
lines changed

4 files changed

+205
-183
lines changed

.github/workflows/build.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,21 @@ jobs:
2929
- name: Set up Cloud SDK
3030
uses: google-github-actions/setup-gcloud@v2
3131

32+
- name: Setup Go
33+
uses: actions/setup-go@v6
34+
with:
35+
go-version-file: 'go.mod'
36+
cache: false
37+
3238
- name: Make tag
3339
run: |
3440
[ "${GITHUB_EVENT_NAME}" == 'pull_request' ] && echo "TARGET_BINARY_LOCATION=pull-requests/$(echo $GITHUB_REF | awk -F / '{print $3}')-${GITHUB_HEAD_REF##*/}" >> $GITHUB_ENV || true
3541
[ "${GITHUB_EVENT_NAME}" == 'release' ] && echo "TARGET_BINARY_LOCATION=${GITHUB_REF##*/}" >> $GITHUB_ENV || true
3642
[ "${GITHUB_EVENT_NAME}" == 'push' ] && echo "TARGET_BINARY_LOCATION=latest" >> $GITHUB_ENV || true
3743
44+
- name: Run integration test
45+
run: make test-integration
46+
3847
- name: Build image
3948
run: |
4049
make metal-hammer-initrd.img.lz4

Makefile

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ LINKMODE := -linkmode external -extldflags '-static -s -w' \
2929
-X 'github.com/metal-stack/v.GitSHA1=$(SHA)' \
3030
-X 'github.com/metal-stack/v.BuildDate=$(BUILDDATE)'
3131

32-
bin/$(BINARY): test $(GOSRC)
32+
bin/$(BINARY): test-unit $(GOSRC)
3333
$(info CGO_ENABLED="$(CGO_ENABLED)")
3434
$(GO) build \
3535
-tags netgo \
@@ -39,10 +39,15 @@ bin/$(BINARY): test $(GOSRC)
3939
$(MAINMODULE)
4040
strip bin/$(BINARY)
4141

42-
.PHONY: test
43-
test:
42+
.PHONY: test-unit
43+
test-unit:
4444
CGO_ENABLED=1 $(GO) test -cover ./...
4545

46+
.PHONY: test-integration
47+
test-integration:
48+
CGO_ENABLED=1 find . -name '*_integration_test.go' -type f -printf '%h\n' | sort -u | xargs -r $(GO) test -cover -tags=integration
49+
# CGO_ENABLED=1 $(GO) test -cover -tags=integration ./.../*_integration_test.go
50+
4651
.PHONY: clean
4752
clean::
4853
rm -f ${INITRD} ${INITRD_COMPRESSED}
@@ -138,4 +143,4 @@ start:
138143
--cmdline "console=ttyS0" \
139144
--cpus boot=4 \
140145
--memory size=1024M \
141-
--net "tap=,mac=,ip=,mask="
146+
--net "tap=,mac=,ip=,mask="
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
//go:build integration
2+
package image
3+
4+
import (
5+
"context"
6+
"crypto/rand"
7+
"fmt"
8+
"log/slog"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
13+
"github.com/foomo/htpasswd"
14+
"github.com/google/go-containerregistry/pkg/authn"
15+
"github.com/google/go-containerregistry/pkg/crane"
16+
"github.com/metal-stack/metal-lib/pkg/pointer"
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/require"
19+
"github.com/testcontainers/testcontainers-go"
20+
"github.com/testcontainers/testcontainers-go/wait"
21+
)
22+
23+
func TestOciPull(t *testing.T) {
24+
var (
25+
assert = assert.New(t)
26+
27+
ctx = context.Background()
28+
mountDir = "/tmp/oci-pull-mount-dir"
29+
extractedBin = "a"
30+
31+
anonymousUsername = ""
32+
anonymousPassword = ""
33+
)
34+
35+
t.Run("successful anonymous pull", func(t *testing.T) {
36+
regIP, regPort, err := startRegistry(nil, nil, nil)
37+
require.NoError(t, err)
38+
registry := fmt.Sprintf("%s:%d", regIP, regPort)
39+
40+
imageRef := fmt.Sprintf("%s/library/image", registry)
41+
err = createImage(imageRef, "", "")
42+
require.NoError(t, err)
43+
44+
err = os.MkdirAll(mountDir, 0777)
45+
require.NoError(t, err)
46+
defer os.RemoveAll(mountDir)
47+
48+
i := NewImage(slog.Default())
49+
if err = i.OciPull(ctx, imageRef, mountDir, anonymousUsername, anonymousPassword); err != nil {
50+
t.Error(err)
51+
}
52+
53+
extractedBinFullPath := filepath.Join(mountDir, extractedBin)
54+
assert.FileExists(extractedBinFullPath)
55+
})
56+
57+
t.Run("successful authenticated pull", func(t *testing.T) {
58+
var (
59+
username = "test-user"
60+
password = "test-password"
61+
)
62+
63+
f, err := os.CreateTemp("", "htpasswd")
64+
require.NoError(t, err)
65+
defer func() {
66+
_ = os.Remove(f.Name())
67+
}()
68+
69+
err = htpasswd.SetPassword(f.Name(), username, password, htpasswd.HashBCrypt)
70+
require.NoError(t, err)
71+
72+
env := map[string]string{
73+
"REGISTRY_AUTH": "htpasswd",
74+
"REGISTRY_AUTH_HTPASSWD_REALM": "registry-login",
75+
"REGISTRY_AUTH_HTPASSWD_PATH": "/htpasswd",
76+
}
77+
regIP, regPort, err := startRegistry(env, pointer.Pointer(f.Name()), pointer.Pointer("/htpasswd"))
78+
require.NoError(t, err)
79+
registry := fmt.Sprintf("%s:%d", regIP, regPort)
80+
81+
imageRefBehindAuth := fmt.Sprintf("%s/library/image", registry)
82+
err = createImage(imageRefBehindAuth, username, password)
83+
require.NoError(t, err)
84+
85+
err = os.MkdirAll(mountDir, 0777)
86+
require.NoError(t, err)
87+
defer os.RemoveAll(mountDir)
88+
89+
i := NewImage(slog.Default())
90+
if err = i.OciPull(ctx, imageRefBehindAuth, mountDir, username, password); err != nil {
91+
t.Error(err)
92+
}
93+
94+
extractedBinFullPath := filepath.Join(mountDir, extractedBin)
95+
assert.FileExists(extractedBinFullPath)
96+
})
97+
98+
t.Run("parsing of image refs fails", func(t *testing.T) {
99+
invalidImageRef := "invalid://"
100+
i := NewImage(slog.Default())
101+
err := i.OciPull(ctx, invalidImageRef, mountDir, anonymousUsername, anonymousPassword)
102+
assert.EqualError(err, "parsing image reference: could not parse reference: invalid://")
103+
})
104+
105+
t.Run("pulling remote image fails", func(t *testing.T) {
106+
imageRefDoesNotExist := "oci://does/not/exist:tag"
107+
i := NewImage(slog.Default())
108+
err := i.OciPull(ctx, imageRefDoesNotExist, mountDir, anonymousUsername, anonymousPassword)
109+
assert.Error(err)
110+
})
111+
}
112+
113+
// HELPER FUNCTIONS
114+
func startRegistry(env map[string]string, src, dst *string) (string, int, error) {
115+
ctx := context.Background()
116+
var (
117+
c testcontainers.Container
118+
err error
119+
)
120+
121+
req := testcontainers.ContainerRequest{
122+
Image: "registry:3",
123+
ExposedPorts: []string{"5000/tcp"},
124+
Env: env,
125+
WaitingFor: wait.ForAll(
126+
wait.ForLog("listening on"),
127+
wait.ForListeningPort("5000/tcp"),
128+
),
129+
}
130+
c, err = testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
131+
ContainerRequest: req,
132+
Started: true,
133+
})
134+
if err != nil {
135+
return "", 0, err
136+
}
137+
if src != nil && dst != nil {
138+
err = c.CopyFileToContainer(ctx, *src, *dst, 0o777)
139+
if err != nil {
140+
return "", 0, err
141+
}
142+
}
143+
144+
ip, err := c.Host(ctx)
145+
if err != nil {
146+
return ip, 0, err
147+
}
148+
port, err := c.MappedPort(ctx, "5000")
149+
if err != nil {
150+
return ip, port.Int(), err
151+
}
152+
153+
return ip, port.Int(), nil
154+
}
155+
156+
func createImage(imageName, username, password string, tags ...string) error {
157+
// ensure every image has distinct content
158+
buf := make([]byte, 128)
159+
_, err := rand.Read(buf)
160+
if err != nil {
161+
return err
162+
}
163+
img, err := crane.Image(map[string][]byte{"a": buf})
164+
if err != nil {
165+
return err
166+
}
167+
168+
var auth = authn.Anonymous
169+
if username != "" || password != "" {
170+
auth = &authn.Basic{
171+
Username: username,
172+
Password: password,
173+
}
174+
}
175+
err = crane.Push(img, imageName, crane.WithAuth(auth))
176+
if err != nil {
177+
return err
178+
}
179+
for _, tag := range tags {
180+
err := crane.Push(img, imageName+":"+tag, crane.WithAuth(auth))
181+
if err != nil {
182+
return err
183+
}
184+
}
185+
186+
return nil
187+
}

0 commit comments

Comments
 (0)