Skip to content

Commit 034b109

Browse files
committed
install skopeo from source, when missing
1 parent 5345a85 commit 034b109

File tree

3 files changed

+142
-6
lines changed

3 files changed

+142
-6
lines changed

Makefile

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ else
109109
GO111MODULE=on go build -o $(GOPATH)/bin/golangci-lint ./vendor/github.com/golangci/golangci-lint/cmd/golangci-lint
110110
endif
111111

112+
SKOPEO := $(shell command -v skopeo 2> /dev/null)
113+
install-skopeo:
114+
ifdef SKOPEO
115+
@echo "Found skopeo"
116+
skopeo --version
117+
else
118+
@echo "Installing skopeo"
119+
./hack/install-skopeo.sh
120+
endif
121+
122+
# install-skopeo is purposely omitted from this target because it is only
123+
# needed for a single test target (test-e2e-ocl).
112124
install-tools: install-golangci-lint install-go-junit-report install-setup-envtest
113125

114126
# Runs golangci-lint
@@ -203,8 +215,9 @@ test-e2e-techpreview: install-go-junit-report
203215
test-e2e-single-node: install-go-junit-report
204216
set -o pipefail; go test -tags=$(GOTAGS) -failfast -timeout 120m -v$${WHAT:+ -run="$$WHAT"} ./test/e2e-single-node/ | ./hack/test-with-junit.sh $(@)
205217

206-
test-e2e-ocl: install-go-junit-report
207-
set -o pipefail; go test -tags=$(GOTAGS) -failfast -timeout 190m -v$${WHAT:+ -run="$$WHAT"} ./test/e2e-ocl/ | ./hack/test-with-junit.sh $(@)
218+
test-e2e-ocl: install-go-junit-report install-skopeo
219+
# Temporarily include /tmp/skopeo/bin in our PATH variable so that the test suite can find skopeo.
220+
set -o pipefail; PATH="$(PATH):/tmp/skopeo/bin" go test -tags=$(GOTAGS) -failfast -timeout 190m -v$${WHAT:+ -run="$$WHAT"} ./test/e2e-ocl/ | ./hack/test-with-junit.sh $(@)
208221

209222
bootstrap-e2e: install-go-junit-report install-setup-envtest
210223
@echo "Setting up KUBEBUILDER_ASSETS"

hack/install-skopeo.sh

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bash
2+
3+
set -xeuo
4+
5+
# This is an installation script for skopeo. skopeo is needed by the
6+
# e2e-gcp-op-ocl test suite since it pushes and pulls images around.
7+
#
8+
# Using a build-root image for this is the preferred approach (see:
9+
# https://docs.ci.openshift.org/docs/architecture/ci-operator/#build-root-image)
10+
# since skopeo can be installed via dnf at build-time. However, that process is
11+
# a bit more involved. While it would ultimately pay off, it is not in-scope
12+
# with the bug that is being resolved.
13+
#
14+
# Because of limitations within the builder container image and the context it
15+
# runs in, certain adaptations must be made:
16+
# - The builder image does not run as a privileged user and is denied privilege
17+
# escalation. This means that running dnf install -y skopeo cannot be done.
18+
# - The builder image does not have jq installed. This means we need to use
19+
# Python for any JSON parsing we need to perform instead.
20+
# - Because this runs as a non-privileged user, we cannot run the make install
21+
# step for skopeo. Instead, we need to append /tmp/skopeo/bin to the PATH. This
22+
# is done in the Makefile and only for the go test invocation.
23+
24+
OPENSHIFT_CI="${OPENSHIFT_CI:-""}"
25+
26+
install_skopeo() {
27+
# If we've already built skopeo once, check if it works and then return.
28+
if [ -f /tmp/skopeo/bin/skopeo ]; then
29+
echo "Prebuilt skopeo found at /tmp/skopeo/bin/skopeo, skipping installation"
30+
/tmp/skopeo/bin/skopeo --version
31+
return 0
32+
fi
33+
34+
# Get the most recent tagged version of skopeo.
35+
skopeo_version="$(curl -s https://api.github.com/repos/containers/skopeo/releases/latest | python3 -c 'import sys, json; print(json.load(sys.stdin)["tag_name"])')"
36+
37+
echo "Installing skopeo $skopeo_version from source"
38+
39+
skopeo_clone_dir="/tmp/skopeo"
40+
41+
mkdir -p "$skopeo_clone_dir"
42+
43+
# Shallow-clone the skopeo repo to the local repo dir.
44+
git clone --branch "$skopeo_version" --depth 1 https://github.com/containers/skopeo.git "$skopeo_clone_dir"
45+
46+
cd "$skopeo_clone_dir"
47+
48+
# Build skopeo
49+
make bin/skopeo
50+
}
51+
52+
# Check if we have skopeo installed first.
53+
if command -v skopeo >/dev/null 2>&1 ; then
54+
# If we do, just output its version.
55+
skopeo --version
56+
else
57+
# Check if we're running in CI.
58+
if [ "$OPENSHIFT_CI" == "true" ]; then
59+
# Only when we're in CI should we install skopeo.
60+
install_skopeo
61+
else
62+
# Otherwise, we should exit with a clear error.
63+
echo "Missing required binary 'skopeo'"
64+
exit 1
65+
fi
66+
fi

test/e2e-ocl/imagepruner_test.go

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"flag"
88
"fmt"
9+
"net/http"
910
"os"
1011
"os/exec"
1112
"path/filepath"
@@ -15,7 +16,9 @@ import (
1516

1617
corev1 "k8s.io/api/core/v1"
1718
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/apimachinery/pkg/util/wait"
1820

21+
"github.com/containers/image/v5/docker"
1922
"github.com/containers/image/v5/types"
2023
"github.com/davecgh/go-spew/spew"
2124
"github.com/distribution/reference"
@@ -141,23 +144,52 @@ func TestImagePrunerOnCluster(t *testing.T) {
141144
certsDir := filepath.Join(t.TempDir())
142145
require.NoError(t, os.WriteFile(filepath.Join(certsDir, externalRegistryHostname+".crt"), ingressCert.Data["tls.crt"], 0o644))
143146

144-
// Wait for the route to finish setting up. Not sure if there is an object we can poll for this instead.
145-
time.Sleep(time.Second * 5)
147+
// Wait for the route to finish setting up. We can determine that the route
148+
// setup is complete when we get an image not found error when inspecting a
149+
// nonexistent image.
150+
err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) {
151+
imgPruner := imagepruner.NewImageInspectorDeleter()
152+
sysCtx := &types.SystemContext{DockerCertPath: certsDir, AuthFilePath: secretPath}
153+
154+
_, _, err = imgPruner.ImageInspect(ctx, sysCtx, pullspec)
155+
156+
// If we get an image not found error, that means the route is set up
157+
// because we were able to authenticate to the image registry and make a
158+
// query for a nonexistent image.
159+
if imagepruner.IsImageNotFoundErr(err) {
160+
return true, nil
161+
}
162+
163+
// If this is an HTTP 503 error, that means the route has not finished
164+
// being set up, so we need to try again.
165+
var unexpectedHTTPError docker.UnexpectedHTTPStatusError
166+
if errors.As(err, &unexpectedHTTPError) && unexpectedHTTPError.StatusCode == http.StatusServiceUnavailable {
167+
return false, nil
168+
}
169+
170+
// We were unable to identify this error, so return it.
171+
return false, fmt.Errorf("unknown registry error when polling: %w", err)
172+
})
173+
174+
require.NoError(t, err, unwrapAll(err))
146175

147176
// Now we can run our test cases. All test cases use the
148177
// ImageInspectorDeleter directly since we need to have a bit more control
149178
// over the SystemContext given that we're running out-of-cluster.
150179
t.Run("Inspect without creds", func(t *testing.T) {
151180
t.Parallel()
181+
152182
imgPruner := imagepruner.NewImageInspectorDeleter()
153183
sysCtx := &types.SystemContext{DockerCertPath: certsDir}
184+
154185
_, _, err = imgPruner.ImageInspect(ctx, sysCtx, pullspec)
155186
assert.Error(t, err)
156187
assert.True(t, imagepruner.IsAccessDeniedErr(err), "expected access denied err: %s", unwrapAll(err))
157188
})
158189

159190
t.Run("Inspect nonexistent image digest with creds", func(t *testing.T) {
160191
t.Parallel()
192+
161193
imgPruner := imagepruner.NewImageInspectorDeleter()
162194
sysCtx := &types.SystemContext{DockerCertPath: certsDir, AuthFilePath: secretPath}
163195

@@ -172,6 +204,7 @@ func TestImagePrunerOnCluster(t *testing.T) {
172204

173205
t.Run("Inspect nonexistent image tag with creds", func(t *testing.T) {
174206
t.Parallel()
207+
175208
imgPruner := imagepruner.NewImageInspectorDeleter()
176209
sysCtx := &types.SystemContext{DockerCertPath: certsDir, AuthFilePath: secretPath}
177210

@@ -185,6 +218,7 @@ func TestImagePrunerOnCluster(t *testing.T) {
185218

186219
t.Run("Inspect nonexistent image repo with creds", func(t *testing.T) {
187220
t.Parallel()
221+
188222
imgPruner := imagepruner.NewImageInspectorDeleter()
189223
sysCtx := &types.SystemContext{DockerCertPath: certsDir, AuthFilePath: secretPath}
190224

@@ -198,9 +232,12 @@ func TestImagePrunerOnCluster(t *testing.T) {
198232

199233
t.Run("Push image and inspect", func(t *testing.T) {
200234
t.Parallel()
235+
201236
require.NoError(t, createAndPushScratchImage(ctx, t, pullspec, secretPath, certsDir))
237+
202238
imgPruner := imagepruner.NewImageInspectorDeleter()
203239
sysCtx := &types.SystemContext{DockerCertPath: certsDir, AuthFilePath: secretPath}
240+
204241
_, _, err := imgPruner.ImageInspect(ctx, sysCtx, pullspec)
205242
assert.NoError(t, err)
206243

@@ -779,6 +816,21 @@ func canTestOnInClusterRegistry(ctx context.Context, kubeconfig string) (bool, e
779816
return false, nil
780817
}
781818

819+
// Skopeo requires that a policy.json file be present. Usually, this file is
820+
// placed in /etc/containers/policy.json when Skopeo is installed. Because we
821+
// must install skopeo from source in CI, this file is missing. So what we do
822+
// in this scenario is write our own policy.json file to a temp directory
823+
// instead. The temp directory is managed by the Go test suite and will be
824+
// removed after the test is finished.
825+
func writePolicyFile(t *testing.T) (string, error) {
826+
policyPath := filepath.Join(t.TempDir(), "policy.json")
827+
828+
// Compacted contents of https://github.com/containers/skopeo/blob/main/default-policy.json
829+
policyJSONBytes := []byte(`{"default":[{"type":"insecureAcceptAnything"}],"transports":{"docker-daemon":{"":[{"type":"insecureAcceptAnything"}]}}}`)
830+
831+
return policyPath, os.WriteFile(policyPath, policyJSONBytes, 0o755)
832+
}
833+
782834
// Creates an empty scratch image and pushes it to the given pullspec using the
783835
// provided secret path. Accepts an optional certsDir parameter which is
784836
// particularly useful for pushing internal image registries which have
@@ -792,9 +844,14 @@ func createAndPushScratchImage(ctx context.Context, t *testing.T, pullspec, secr
792844
return err
793845
}
794846

795-
cmd := exec.Command("skopeo", "copy", "--dest-authfile", secretPath, "tarball://"+srcImage, "docker://"+pullspec)
847+
policyPath, err := writePolicyFile(t)
848+
if err != nil {
849+
return fmt.Errorf("could not write policy.json file: %w", err)
850+
}
851+
852+
cmd := exec.Command("skopeo", "--policy", policyPath, "copy", "--dest-authfile", secretPath, "tarball://"+srcImage, "docker://"+pullspec)
796853
if certsDir != "" {
797-
cmd = exec.Command("skopeo", "copy", "--dest-cert-dir", certsDir, "--dest-authfile", secretPath, "tarball://"+srcImage, "docker://"+pullspec)
854+
cmd = exec.Command("skopeo", "--policy", policyPath, "copy", "--dest-cert-dir", certsDir, "--dest-authfile", secretPath, "tarball://"+srcImage, "docker://"+pullspec)
798855
}
799856

800857
t.Logf("Copying %s to %s using skopeo", srcImage, pullspec)

0 commit comments

Comments
 (0)