Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c1571f1960 | |||
| a161c44575 |
@@ -4,3 +4,6 @@ bin/
|
||||
# e2e-docker fixtures are real package files (.rpm, .tgz, .whl, .zip, ...) that
|
||||
# are intentionally tracked, overriding any global ignore of those extensions.
|
||||
!e2e-docker/fixtures/**
|
||||
|
||||
# Written per run by the dockerised e2e TestMain.
|
||||
e2e-docker/oidc/jwks.json
|
||||
|
||||
@@ -21,7 +21,17 @@ services:
|
||||
volumes:
|
||||
- ./e2e-docker/mirror-conf/b.conf:/etc/nginx/conf.d/default.conf:ro,z
|
||||
|
||||
# Static OIDC issuer gating writes; the suite's TestMain writes jwks.json.
|
||||
mockoidc:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./e2e-docker/oidc:/usr/share/nginx/html:ro,z
|
||||
|
||||
artifactapi:
|
||||
environment:
|
||||
ARTIFACTAPI_OIDC_ISSUER: http://mockoidc
|
||||
ARTIFACTAPI_OIDC_AUDIENCE: artifactapi
|
||||
ARTIFACTAPI_AUTH_UPLOADS: "true"
|
||||
# The host port is set via ARTIFACTAPI_PORT (see scripts/docker-e2e.sh),
|
||||
# defaulting to 8000; the e2e run uses 8001 to avoid colliding with a
|
||||
# locally-running instance.
|
||||
@@ -32,3 +42,5 @@ services:
|
||||
condition: service_started
|
||||
mockupstreamb:
|
||||
condition: service_started
|
||||
mockoidc:
|
||||
condition: service_started
|
||||
|
||||
@@ -21,6 +21,13 @@ already-running stack.
|
||||
|
||||
## Coverage
|
||||
|
||||
- **Write auth** — the stack runs with `ARTIFACTAPI_AUTH_UPLOADS=true` against a
|
||||
static `mockoidc` issuer (`oidc/`); `TestMain` generates the signing key, writes
|
||||
`oidc/jwks.json` and mints the tokens. Every local publish authenticates the way
|
||||
its real client does (curl Bearer/Basic, `docker login`, npm `_authToken`,
|
||||
`GEM_HOST_API_KEY`, `ansible-galaxy --api-key`), and each type checks a
|
||||
publish without a token or with a foreign-key token is refused while
|
||||
anonymous reads still work.
|
||||
- **Repository lifecycle** — add / change / delete for remote, local and virtual repos.
|
||||
- **Caching** — one immutable artifact per remote package type (one
|
||||
`caching_<type>_test.go` case each) proxied through the mock upstream: first fetch `X-Artifact-Source: remote`, second `cache`, bytes
|
||||
|
||||
@@ -47,18 +47,20 @@ func TestLocalAnsiblePublishInstall(t *testing.T) {
|
||||
defer deleteRepo(t, name)
|
||||
|
||||
script := "set -euo pipefail\n" + ansibleHelloScript + `
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key unused
|
||||
if ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key unused; then echo "republish accepted"; exit 1; fi
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key "$TOKEN"
|
||||
if ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key "$TOKEN"; then echo "republish accepted"; exit 1; fi
|
||||
cd /w/unkin/e2e_hello && sed -i 's/^version: .*/version: 1.0.1/' galaxy.yml && ansible-galaxy collection build --output-path /w/dist >/dev/null
|
||||
if ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.1.tar.gz --server "$SRV" --api-key "$BAD"; then echo "invalid key publish accepted"; exit 1; fi
|
||||
ansible-galaxy collection install unkin.e2e_hello:==1.0.0 --server "$SRV" -p /w/coll
|
||||
` + ansibleHelloPlay
|
||||
|
||||
srv := strings.TrimRight(internal, "/") + "/api/v1/local/" + name + "/"
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "SRV="+srv, "python:3.12-slim", "bash", "-c", script).CombinedOutput()
|
||||
"-e", "SRV="+srv, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken, "python:3.12-slim", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("ansible-galaxy client run failed: %v\n%s", err, out)
|
||||
}
|
||||
for _, want := range []string{"successfully published and imported", "HTTP Code: 409", "unkin.e2e_hello:1.0.0 was installed successfully", "PLAY hello e2e"} {
|
||||
for _, want := range []string{"successfully published and imported", "HTTP Code: 409", "HTTP Code: 401", "unkin.e2e_hello:1.0.0 was installed successfully", "PLAY hello e2e"} {
|
||||
if !strings.Contains(string(out), want) {
|
||||
t.Fatalf("missing %q in client output:\n%s", want, out)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestVirtualAnsibleInstall(t *testing.T) {
|
||||
|
||||
base := strings.TrimRight(internal, "/")
|
||||
script := "set -euo pipefail\n" + ansibleHelloScript + `
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$BASE/api/v1/local/$B/" --api-key unused
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$BASE/api/v1/local/$B/" --api-key "$TOKEN"
|
||||
V="$BASE/api/v1/virtual/$VIRT"
|
||||
ansible-galaxy collection install unkin.e2e_hello --server "$V/" -p /w/coll
|
||||
DL=$(python3 -c 'import json,sys,urllib.request as u;print(json.load(u.urlopen(sys.argv[1]))["download_url"])' "$V/api/v3/collections/unkin/e2e_hello/versions/1.0.0/")
|
||||
@@ -47,7 +47,7 @@ except u.HTTPError as e: print(e.code, e.headers["Location"])' "$DL")"
|
||||
` + ansibleHelloPlay
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "BASE="+base, "-e", "B="+b, "-e", "VIRT="+virt,
|
||||
"-e", "BASE="+base, "-e", "B="+b, "-e", "VIRT="+virt, "-e", "TOKEN="+authToken,
|
||||
"python:3.12-slim", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("ansible-galaxy client run failed: %v\n%s", err, out)
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -79,8 +81,12 @@ func TestLocalDockerPushPull(t *testing.T) {
|
||||
const image = "team/app"
|
||||
const tag = "v1.0.0"
|
||||
|
||||
// /v2/ version check.
|
||||
resp, _ := doRequest(t, http.MethodGet, api("/v2/"), nil, "")
|
||||
// /v2/ version check: anonymous gets the token challenge, a token passes.
|
||||
resp, _ := doRequestAuth(t, http.MethodGet, api("/v2/"), nil, "", "")
|
||||
if resp.StatusCode != http.StatusUnauthorized || !strings.Contains(resp.Header.Get("WWW-Authenticate"), "/v2/token") {
|
||||
t.Fatalf("anonymous /v2/ ping: status %d, challenge %q", resp.StatusCode, resp.Header.Get("WWW-Authenticate"))
|
||||
}
|
||||
resp, _ = doRequest(t, http.MethodGet, api("/v2/"), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("/v2/ ping: status %d", resp.StatusCode)
|
||||
}
|
||||
@@ -169,9 +175,66 @@ func TestLocalDockerPushPull(t *testing.T) {
|
||||
t.Fatalf("tags/list wrong repository name: %s", body)
|
||||
}
|
||||
|
||||
assertWriteGated(t, http.MethodPut, api("/v2/docker-internal/"+image+"/manifests/v2"), manifest, manifestType, api("/v2/docker-internal/"+image+"/manifests/"+tag))
|
||||
|
||||
// A now-present blob HEAD should succeed (client would skip re-upload).
|
||||
resp, _ = doRequest(t, http.MethodHead, api("/v2/docker-internal/"+image+"/blobs/"+layerDigest), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("post-push blob HEAD: expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalDockerCLIPushPull drives the host docker CLI: docker login with the
|
||||
// token as password then push, a push without login rejected, an anonymous
|
||||
// pull, and a buildx (buildkit) push.
|
||||
func TestLocalDockerCLIPushPull(t *testing.T) {
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
t.Skip("docker not available on the test host")
|
||||
}
|
||||
createRepo(t, `{"name":"docker-cli","package_type":"docker","repo_type":"local"}`)
|
||||
defer deleteRepo(t, "docker-cli")
|
||||
|
||||
reg := strings.TrimPrefix(baseURL(), "http://")
|
||||
img := reg + "/docker-cli/e2e/hello"
|
||||
cfg := t.TempDir()
|
||||
docker := func(stdin []byte, args ...string) (string, error) {
|
||||
cmd := exec.Command("docker", args...)
|
||||
cmd.Env = append(os.Environ(), "DOCKER_CONFIG="+cfg)
|
||||
cmd.Stdin = bytes.NewReader(stdin)
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
must := func(t *testing.T, stdin []byte, args ...string) {
|
||||
t.Helper()
|
||||
if out, err := docker(stdin, args...); err != nil {
|
||||
t.Fatalf("docker %s: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
}
|
||||
defer func() { _, _ = docker(nil, "rmi", "-f", img+":v1", img+":anon", img+":buildx") }()
|
||||
|
||||
must(t, make([]byte, 1024), "import", "-", img+":v1") // an empty tar
|
||||
|
||||
must(t, []byte(authToken), "login", reg, "-u", "ci", "--password-stdin")
|
||||
must(t, nil, "push", img+":v1")
|
||||
|
||||
must(t, nil, "logout", reg)
|
||||
must(t, nil, "tag", img+":v1", img+":anon")
|
||||
if out, err := docker(nil, "push", img+":anon"); err == nil {
|
||||
t.Fatalf("push without login accepted:\n%s", out)
|
||||
}
|
||||
must(t, nil, "rmi", img+":v1", img+":anon")
|
||||
must(t, nil, "pull", img+":v1")
|
||||
|
||||
t.Run("buildx", func(t *testing.T) {
|
||||
t.Skip("buildkit fetches the registry token with an OAuth2 POST form the server ignores; fixed by unkin/artifactapi#180")
|
||||
conf := cfg + "/buildkitd.toml"
|
||||
if err := os.WriteFile(conf, []byte(fmt.Sprintf("[registry.%q]\n http = true\n", reg)), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
builder := fmt.Sprintf("e2e-%d", os.Getpid())
|
||||
must(t, nil, "buildx", "create", "--name", builder, "--driver", "docker-container", "--driver-opt", "network=host", "--buildkitd-config", conf)
|
||||
defer func() { _, _ = docker(nil, "buildx", "rm", "-f", builder) }()
|
||||
must(t, []byte(authToken), "login", reg, "-u", "ci", "--password-stdin")
|
||||
must(t, []byte("FROM "+img+":v1\nLABEL e2e=buildx\n"), "buildx", "build", "--builder", builder, "--push", "-t", img+":buildx", "-")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestLocalGemsPushInstall(t *testing.T) {
|
||||
|
||||
src := strings.TrimRight(internal, "/") + "/api/v1/local/" + name
|
||||
script := `set -euo pipefail
|
||||
export GEM_HOST_API_KEY=unused
|
||||
export GEM_HOST_API_KEY="$TOKEN"
|
||||
mk() { d=/w/src-$1${2:+-b}; mkdir -p $d/lib; cd $d
|
||||
echo "module UnkinE2eHello; VERSION = '$1'; end${2:-}" > lib/unkin-e2e-hello.rb
|
||||
printf 'Gem::Specification.new { |s| s.name = "unkin-e2e-hello"; s.version = "%s"; s.summary = "e2e"; s.authors = ["e2e"]; s.files = ["lib/unkin-e2e-hello.rb"] }' "$1" > g.gemspec
|
||||
@@ -52,14 +52,15 @@ echo "INFO $(curl -s "$SRC/info/unkin-e2e-hello" | cut -d' ' -f1 | tr '\n' ' ')"
|
||||
mk 0.2.0 '# changed' && if gem push --host "$SRC" unkin-e2e-hello-0.2.0.gem; then echo "yanked re-push accepted"; exit 1; fi
|
||||
cd /w/app && gem uninstall -a -x unkin-e2e-hello >/dev/null
|
||||
if gem install --remote --no-document unkin-e2e-hello -v 0.2.0; then echo "yanked version installed"; exit 1; fi
|
||||
mk 0.3.0 && if GEM_HOST_API_KEY="$BAD" gem push --host "$SRC" unkin-e2e-hello-0.3.0.gem; then echo "invalid key push accepted"; exit 1; fi
|
||||
echo "VERSIONS $(curl -s "$SRC/versions" | grep '^unkin-e2e-hello ' | cut -d' ' -f2)"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "SRC="+src, "ruby:3.3", "bash", "-c", script).CombinedOutput()
|
||||
"-e", "SRC="+src, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken, "ruby:3.3", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("gem client run failed: %v\n%s", err, out)
|
||||
}
|
||||
for _, want := range []string{"GEM 0.2.0", "BUNDLE 0.2.0", "INFO --- 0.1.0 ", "re-pushing a yanked version is not allowed", "VERSIONS 0.1.0\n"} {
|
||||
for _, want := range []string{"GEM 0.2.0", "BUNDLE 0.2.0", "INFO --- 0.1.0 ", "re-pushing a yanked version is not allowed", `{"error":"invalid token"}`, "VERSIONS 0.1.0\n"} {
|
||||
if !strings.Contains(string(out), want) {
|
||||
t.Fatalf("missing %q in gem output:\n%s", want, out)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestVirtualGemsInstall(t *testing.T) {
|
||||
|
||||
base := strings.TrimRight(internal, "/")
|
||||
script := `set -euo pipefail
|
||||
export GEM_HOST_API_KEY=unused
|
||||
export GEM_HOST_API_KEY="$TOKEN"
|
||||
pub() { d=/w/src-$1-$2; mkdir -p $d/lib; cd $d
|
||||
echo "module UnkinE2eHello; VERSION = '$1:$2'; end" > lib/unkin-e2e-hello.rb
|
||||
printf 'Gem::Specification.new { |s| s.name = "unkin-e2e-hello"; s.version = "%s"; s.summary = "e2e"; s.authors = ["e2e"]; s.files = ["lib/unkin-e2e-hello.rb"] }' "$2" > g.gemspec
|
||||
@@ -52,7 +52,7 @@ bundle exec ruby -e 'require "unkin-e2e-hello"; puts "REQ #{UnkinE2eHello::VERSI
|
||||
echo "REDIRECT $(curl -s -o /dev/null -w '%{redirect_url}' "$V/gems/unkin-e2e-hello-0.2.0.gem")"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt, "-e", "TOKEN="+authToken,
|
||||
"ruby:3.3", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("bundler run failed: %v\n%s", err, out)
|
||||
|
||||
@@ -37,6 +37,8 @@ func TestLocalGoProxy(t *testing.T) {
|
||||
uploadFile(t, "local-go", mod+"/@v/"+ver+".mod", gomod, "text/plain")
|
||||
}
|
||||
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-go/files/"+mod+"/@v/v0.3.0.mod"), gomod, "text/plain", api("/api/v1/local/local-go/"+mod+"/@v/list"))
|
||||
|
||||
resp, body := doRequest(t, http.MethodPut, api("/api/v2/remotes/local-go/files/"+mod+"/@v/v0.3.0.zip"), []byte("junk"), "application/zip")
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("invalid zip: status %d: %s", resp.StatusCode, body)
|
||||
|
||||
@@ -29,10 +29,10 @@ func TestLocalHelmRealClient(t *testing.T) {
|
||||
createVirtual(t, `{"name":"vhelm","package_type":"helm","members":["local-helm"]}`)
|
||||
defer deleteVirtual(t, "vhelm")
|
||||
|
||||
helm := func(script string) string {
|
||||
helm := func(t *testing.T, script string) string {
|
||||
t.Helper()
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network, "--entrypoint", "sh",
|
||||
"alpine/helm:3.16.4", "-c", "set -eu; "+script).Output()
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken,
|
||||
"--entrypoint", "sh", "alpine/helm:3.16.4", "-c", "set -eu; "+script).Output()
|
||||
if err != nil {
|
||||
stderr := ""
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
@@ -43,7 +43,7 @@ func TestLocalHelmRealClient(t *testing.T) {
|
||||
return string(out)
|
||||
}
|
||||
|
||||
chart := helm("cd /tmp && helm create e2echart >/dev/null && helm package e2echart >/dev/null && cat e2echart-0.1.0.tgz")
|
||||
chart := helm(t, "cd /tmp && helm create e2echart >/dev/null && helm package e2echart >/dev/null && cat e2echart-0.1.0.tgz")
|
||||
uploadFile(t, "local-helm", "e2echart-0.1.0.tgz", []byte(chart), "application/gzip")
|
||||
|
||||
// Chart metadata is recorded asynchronously after upload; poll the index.
|
||||
@@ -58,14 +58,32 @@ func TestLocalHelmRealClient(t *testing.T) {
|
||||
if !strings.Contains(string(body), "e2echart-0.1.0.tgz") || !strings.Contains(string(body), "digest: ") {
|
||||
t.Fatalf("index.yaml never listed the chart:\n%s", body)
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-helm/files/e2echart-0.2.0.tgz"), []byte(chart), "application/gzip", api("/api/v1/local/local-helm/index.yaml"))
|
||||
|
||||
for _, repoURL := range []string{internal + "/api/v1/local/local-helm", internal + "/api/v1/virtual/vhelm"} {
|
||||
out := helm("helm repo add e2e " + repoURL + " >/dev/null && " +
|
||||
"helm search repo e2e/e2echart && " +
|
||||
"cd /tmp && helm pull e2e/e2echart --version 0.1.0 && " +
|
||||
out := helm(t, "helm repo add e2e "+repoURL+" >/dev/null && "+
|
||||
"helm search repo e2e/e2echart && "+
|
||||
"cd /tmp && helm pull e2e/e2echart --version 0.1.0 && "+
|
||||
"helm template rel e2echart-0.1.0.tgz | grep -c 'kind: Deployment'")
|
||||
if !strings.Contains(out, "e2e/e2echart") || !strings.Contains(out, "0.1.0") || !strings.HasSuffix(strings.TrimSpace(out), "1") {
|
||||
t.Fatalf("helm via %s:\n%s", repoURL, out)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("oci", func(t *testing.T) {
|
||||
t.Skip("helm (oras-go) fetches the registry token with an OAuth2 POST form the server ignores; fixed by unkin/artifactapi#180")
|
||||
createRepo(t, `{"name":"local-helm-oci","package_type":"docker","repo_type":"local"}`)
|
||||
defer deleteRepo(t, "local-helm-oci")
|
||||
reg := strings.TrimPrefix(internal, "http://")
|
||||
out := helm(t, "cd /tmp && helm create e2echart >/dev/null && helm package e2echart >/dev/null && "+
|
||||
"echo \"$BAD\" | helm registry login "+reg+" -u ci --password-stdin --insecure && "+
|
||||
"if helm push e2echart-0.1.0.tgz oci://"+reg+"/local-helm-oci --plain-http; then echo 'invalid token push accepted'; exit 1; fi && "+
|
||||
"echo \"$TOKEN\" | helm registry login "+reg+" -u ci --password-stdin --insecure && "+
|
||||
"helm push e2echart-0.1.0.tgz oci://"+reg+"/local-helm-oci --plain-http && "+
|
||||
"helm registry logout "+reg+" && rm -rf e2echart* && "+
|
||||
"helm pull oci://"+reg+"/local-helm-oci/e2echart --version 0.1.0 --plain-http && ls e2echart-0.1.0.tgz")
|
||||
if !strings.Contains(out, "e2echart-0.1.0.tgz") {
|
||||
t.Fatalf("helm OCI push/pull:\n%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,15 +7,67 @@ package e2edocker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
)
|
||||
|
||||
// authToken is accepted by the stack's write gate; badToken carries the same
|
||||
// claims signed by a key the issuer never published.
|
||||
var authToken, badToken string
|
||||
|
||||
// TestMain publishes a fresh signing key through the static mockoidc issuer
|
||||
// (e2e-docker/oidc, served by docker-compose.e2e.yml) and mints both tokens.
|
||||
func TestMain(m *testing.M) {
|
||||
key := mustRSAKey()
|
||||
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: &key.PublicKey, KeyID: "e2e", Algorithm: "RS256", Use: "sig"}}})
|
||||
if err := os.WriteFile(filepath.Join("oidc", "jwks.json"), jwks, 0o644); err != nil {
|
||||
log.Fatalf("write jwks: %v", err)
|
||||
}
|
||||
authToken, badToken = mintToken(key), mintToken(mustRSAKey())
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func mustRSAKey() *rsa.PrivateKey {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
log.Fatalf("generate key: %v", err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func mintToken(key *rsa.PrivateKey) string {
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: jose.JSONWebKey{Key: key, KeyID: "e2e"}}, (&jose.SignerOptions{}).WithType("JWT"))
|
||||
if err != nil {
|
||||
log.Fatalf("signer: %v", err)
|
||||
}
|
||||
now := time.Now()
|
||||
tok, err := jwt.Signed(sig).Claims(jwt.Claims{
|
||||
Issuer: "http://mockoidc", Subject: "e2e", Audience: jwt.Audience{"artifactapi"},
|
||||
IssuedAt: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
}).Serialize()
|
||||
if err != nil {
|
||||
log.Fatalf("sign token: %v", err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
func basicAuth(token string) string {
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte("ci:"+token))
|
||||
}
|
||||
|
||||
func baseURL() string {
|
||||
if v := os.Getenv("ARTIFACTAPI_URL"); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
@@ -43,7 +95,14 @@ func fixtureBytes(t *testing.T, rel string) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
// doRequest sends authToken as a Bearer token; doRequestAuth sends authz
|
||||
// verbatim, or no Authorization header when it is empty.
|
||||
func doRequest(t *testing.T, method, url string, body []byte, contentType string) (*http.Response, []byte) {
|
||||
t.Helper()
|
||||
return doRequestAuth(t, method, url, body, contentType, "Bearer "+authToken)
|
||||
}
|
||||
|
||||
func doRequestAuth(t *testing.T, method, url string, body []byte, contentType, authz string) (*http.Response, []byte) {
|
||||
t.Helper()
|
||||
var r io.Reader
|
||||
if body != nil {
|
||||
@@ -56,6 +115,9 @@ func doRequest(t *testing.T, method, url string, body []byte, contentType string
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
if authz != "" {
|
||||
req.Header.Set("Authorization", authz)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", method, url, err)
|
||||
@@ -106,3 +168,17 @@ func getEventually(t *testing.T, url string, timeout time.Duration) (*http.Respo
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// assertWriteGated checks a write is refused with no token and with a token
|
||||
// from an unknown key, and that readURL still serves anonymously.
|
||||
func assertWriteGated(t *testing.T, method, url string, body []byte, contentType, readURL string) {
|
||||
t.Helper()
|
||||
for _, authz := range []string{"", "Bearer " + badToken, basicAuth(badToken)} {
|
||||
if resp, b := doRequestAuth(t, method, url, body, contentType, authz); resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("%s %s with %.12q: status %d, want 401: %s", method, url, authz, resp.StatusCode, b)
|
||||
}
|
||||
}
|
||||
if resp, b := doRequestAuth(t, http.MethodGet, readURL, nil, "", ""); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("anonymous GET %s: status %d: %s", readURL, resp.StatusCode, b)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,11 @@ import (
|
||||
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
||||
)
|
||||
|
||||
// uploadFile PUTs with Basic credentials, as `curl -u ci:$TOKEN` does.
|
||||
func uploadFile(t *testing.T, repo, filePath string, body []byte, contentType string) {
|
||||
t.Helper()
|
||||
url := api("/api/v2/remotes/" + repo + "/files/" + filePath)
|
||||
resp, respBody := doRequest(t, http.MethodPut, url, body, contentType)
|
||||
resp, respBody := doRequestAuth(t, http.MethodPut, url, body, contentType, basicAuth(authToken))
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("upload %s: status %d: %s", filePath, resp.StatusCode, respBody)
|
||||
}
|
||||
@@ -31,6 +32,7 @@ func TestLocalGenericUpload(t *testing.T) {
|
||||
|
||||
content := []byte("artifactapi local generic upload payload")
|
||||
uploadFile(t, "local-generic", "data/hello.bin", content, "application/octet-stream")
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-generic/files/data/other.bin"), content, "application/octet-stream", api("/api/v1/local/local-generic/data/hello.bin"))
|
||||
|
||||
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-generic/data/hello.bin"), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -41,6 +43,18 @@ func TestLocalGenericUpload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalTerraformUpload uploads a provider zip the way a release pipeline
|
||||
// does and downloads it back anonymously.
|
||||
func TestLocalTerraformUpload(t *testing.T) {
|
||||
createRepo(t, `{"name":"local-terraform","package_type":"terraform","repo_type":"local"}`)
|
||||
defer deleteRepo(t, "local-terraform")
|
||||
|
||||
const path = "unkin/e2e/terraform-provider-e2e_1.0.0_linux_amd64.zip"
|
||||
zip := []byte("PK\x05\x06" + strings.Repeat("\x00", 18))
|
||||
uploadFile(t, "local-terraform", path, zip, "application/zip")
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-terraform/files/unkin/e2e/terraform-provider-e2e_1.1.0_linux_amd64.zip"), zip, "application/zip", api("/api/v2/remotes/local-terraform/files/"+path))
|
||||
}
|
||||
|
||||
// TestLocalPyPIUpload uploads a wheel and validates the generated simple index.
|
||||
func TestLocalPyPIUpload(t *testing.T) {
|
||||
createRepo(t, `{"name":"local-pypi","package_type":"pypi","repo_type":"local"}`)
|
||||
@@ -48,6 +62,7 @@ func TestLocalPyPIUpload(t *testing.T) {
|
||||
|
||||
wheel := fixtureBytes(t, "packages/foo-1.0-py3-none-any.whl")
|
||||
uploadFile(t, "local-pypi", "foo-1.0-py3-none-any.whl", wheel, "application/zip")
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-pypi/files/foo-2.0-py3-none-any.whl"), wheel, "application/zip", api("/api/v1/local/local-pypi/simple/foo/"))
|
||||
|
||||
// Root index lists the package.
|
||||
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-pypi/simple/"), nil, "")
|
||||
@@ -95,6 +110,7 @@ func TestLocalRPMRepodata(t *testing.T) {
|
||||
if !strings.Contains(s, "<repomd") || !strings.Contains(s, "primary") {
|
||||
t.Fatalf("repomd.xml not a valid repodata document: %s", s)
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-rpm/files/e2e-testpkg-1.0-1.noarch.rpm"), rpm, "application/x-rpm", api("/api/v1/local/local-rpm/repodata/repomd.xml"))
|
||||
}
|
||||
|
||||
// TestLocalDebRepo uploads a .deb and validates that the flat apt index
|
||||
@@ -138,6 +154,7 @@ func TestLocalDebRepo(t *testing.T) {
|
||||
if !bytes.Equal(body, deb) {
|
||||
t.Fatalf("deb content mismatch")
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-deb/files/e2e-testpkg_2.0.0_amd64.deb"), deb, "application/vnd.debian.binary-package", api("/api/v1/local/local-deb/Packages"))
|
||||
}
|
||||
|
||||
// TestLocalAlpineIndex uploads an .apk to an alpine local repo and validates
|
||||
@@ -174,6 +191,7 @@ func TestLocalAlpineIndex(t *testing.T) {
|
||||
if !bytes.Equal(body, apk) {
|
||||
t.Fatalf("apk content mismatch")
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-alpine/files/x86_64/e2e-testpkg-2.0-r0.apk"), apk, "application/vnd.android.package-archive", api("/api/v1/local/local-alpine/x86_64/APKINDEX.tar.gz"))
|
||||
}
|
||||
|
||||
func apkIndexText(t *testing.T, body []byte) string {
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestLocalNPMPublishInstall(t *testing.T) {
|
||||
|
||||
reg := strings.TrimRight(internal, "/") + "/api/v1/local/" + name + "/"
|
||||
script := `set -euo pipefail
|
||||
printf 'registry=%s\n%s:_authToken=unused\n' "$REG" "${REG#http:}" > ~/.npmrc
|
||||
printf 'registry=%s\n%s:_authToken=%s\n' "$REG" "${REG#http:}" "$TOKEN" > ~/.npmrc
|
||||
mk() { d="/w/src/$1-$2"; mkdir -p "$d"; cd "$d"
|
||||
printf '{"name":"%s","version":"%s","main":"index.js"}' "$1" "$2" > package.json
|
||||
echo "module.exports = '$1@$2';" > index.js; }
|
||||
@@ -43,10 +43,15 @@ npm install --no-audit --cache /tmp/fresh-cache e2e-npm-plain @e2e/npm-scoped
|
||||
node -e "console.log('REQ', require('e2e-npm-plain'), require('@e2e/npm-scoped'))"
|
||||
mk e2e-npm-plain 1.1.0 && npm publish
|
||||
echo "VERSIONS $(npm view e2e-npm-plain versions --json | tr -d ' \n')"
|
||||
echo "LATEST $(npm view e2e-npm-plain dist-tags.latest)"`
|
||||
echo "LATEST $(npm view e2e-npm-plain dist-tags.latest)"
|
||||
printf 'registry=%s\n%s:_authToken=%s\n' "$REG" "${REG#http:}" "$BAD" > ~/.npmrc
|
||||
mk e2e-npm-plain 2.0.0
|
||||
if npm publish; then echo "invalid token publish accepted"; exit 1; fi
|
||||
printf 'registry=%s\n' "$REG" > ~/.npmrc
|
||||
echo "ANON $(npm view e2e-npm-plain dist-tags.latest)"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "REG="+reg, "node:22", "bash", "-c", script).CombinedOutput()
|
||||
"-e", "REG="+reg, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken, "node:22", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("npm client run failed: %v\n%s", err, out)
|
||||
}
|
||||
@@ -54,6 +59,8 @@ echo "LATEST $(npm view e2e-npm-plain dist-tags.latest)"`
|
||||
"REQ e2e-npm-plain@1.0.0 @e2e/npm-scoped@1.0.0",
|
||||
`VERSIONS ["1.0.0","1.1.0"]`,
|
||||
"LATEST 1.1.0",
|
||||
"code E401",
|
||||
"ANON 1.1.0",
|
||||
} {
|
||||
if !strings.Contains(string(out), want) {
|
||||
t.Fatalf("missing %q in npm output:\n%s", want, out)
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestVirtualNPMInstall(t *testing.T) {
|
||||
pub() { d="/w/src/$2-$3-$1"; mkdir -p "$d"; cd "$d"
|
||||
printf '{"name":"%s","version":"%s","main":"index.js"}' "$2" "$3" > package.json
|
||||
echo "module.exports = '$1:$2@$3';" > index.js
|
||||
printf 'registry=%s\n%s:_authToken=unused\n' "$BASE/api/v1/local/$1/" "${BASE#http:}/api/v1/local/$1/" > .npmrc
|
||||
printf 'registry=%s\n%s:_authToken=%s\n' "$BASE/api/v1/local/$1/" "${BASE#http:}/api/v1/local/$1/" "$TOKEN" > .npmrc
|
||||
npm publish; }
|
||||
pub "$A" @e2e/virt 1.0.0
|
||||
pub "$B" @e2e/virt 1.0.0
|
||||
@@ -51,7 +51,7 @@ node -e "console.log('REQ', require('@e2e/virt'))"
|
||||
echo "RESOLVED $(node -p "require('./package-lock.json').packages['node_modules/@e2e/virt'].resolved")"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt, "-e", "TOKEN="+authToken,
|
||||
"node:22", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("npm client run failed: %v\n%s", err, out)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"issuer":"http://mockoidc","jwks_uri":"http://mockoidc/jwks.json","authorization_endpoint":"http://mockoidc/authorize","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"]}
|
||||
@@ -68,6 +68,7 @@ func TestLocalPuppetRealClient(t *testing.T) {
|
||||
if resp, body := getEventually(t, api("/api/v1/local/local-puppet/v3/releases/unkin-hello-0.1.0"), 15*time.Second); resp.StatusCode != http.StatusOK || !strings.Contains(string(body), `"file_md5"`) {
|
||||
t.Fatalf("release never indexed: %d %s", resp.StatusCode, body)
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-puppet/files/unkin-hello-0.2.0.tar.gz"), tarball, "application/gzip", api("/api/v1/local/local-puppet/v3/releases/unkin-hello-0.1.0"))
|
||||
|
||||
forge := internal + "/api/v1/local/local-puppet"
|
||||
run := func(image, script string) string {
|
||||
|
||||
@@ -7,7 +7,6 @@ require (
|
||||
github.com/cavaliergopher/rpm v1.3.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/containerd/containerd/v2 v2.2.1
|
||||
github.com/coreos/go-oidc/v3 v3.21.0
|
||||
github.com/go-chi/chi/v5 v5.3.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.4
|
||||
@@ -15,8 +14,6 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.10.0
|
||||
github.com/klauspost/compress v1.19.2
|
||||
github.com/minio/minio-go/v7 v7.2.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/redis/go-redis/v9 v9.20.0
|
||||
github.com/testcontainers/testcontainers-go v0.44.0
|
||||
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0
|
||||
@@ -45,8 +42,7 @@ require (
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.2 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
@@ -59,7 +55,6 @@ require (
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
@@ -76,7 +71,6 @@ require (
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/moby/api v1.55.0 // indirect
|
||||
github.com/moby/moby/client v0.5.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
@@ -87,6 +81,8 @@ require (
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
@@ -112,6 +108,5 @@ require (
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.2 // indirect
|
||||
)
|
||||
|
||||
@@ -36,18 +36,14 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk=
|
||||
github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
|
||||
github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
|
||||
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
|
||||
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
|
||||
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
@@ -83,8 +79,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -97,8 +91,6 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
|
||||
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
@@ -136,8 +128,6 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
||||
@@ -209,8 +199,6 @@ github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
|
||||
github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
@@ -235,34 +223,19 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -272,24 +245,12 @@ golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
+2
-15
@@ -186,7 +186,7 @@ func (b *BearerOIDC) RequireRegistryWrites(next http.Handler, realm func(*http.R
|
||||
hasAuth := r.Header.Get("Authorization") != ""
|
||||
switch {
|
||||
case r.URL.Path == "/v2/token":
|
||||
writeToken(w, b.registryToken(w, r))
|
||||
writeToken(w, b.registryToken(r))
|
||||
case r.URL.Path == "/v2/" && !hasAuth:
|
||||
unauthorized(w, challenge, "authentication required")
|
||||
default:
|
||||
@@ -198,18 +198,8 @@ func (b *BearerOIDC) RequireRegistryWrites(next http.Handler, realm func(*http.R
|
||||
// registryToken echoes a valid credential back as the registry token. Missing
|
||||
// or invalid credentials (e.g. an expired `docker login`) get the anonymous
|
||||
// token so pulls keep working; writes are still verified per request.
|
||||
// containerd/buildkit POST an OAuth2 password or refresh_token grant form;
|
||||
// only a capped urlencoded body is read, never multipart.
|
||||
func (b *BearerOIDC) registryToken(w http.ResponseWriter, r *http.Request) string {
|
||||
func (b *BearerOIDC) registryToken(r *http.Request) string {
|
||||
raw := token(r)
|
||||
if raw == "" && r.Method == http.MethodPost {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxTokenForm)
|
||||
if r.ParseForm() == nil {
|
||||
if raw = r.PostForm.Get("password"); raw == "" {
|
||||
raw = r.PostForm.Get("refresh_token")
|
||||
}
|
||||
}
|
||||
}
|
||||
if raw == "" {
|
||||
return "anonymous"
|
||||
}
|
||||
@@ -220,9 +210,6 @@ func (b *BearerOIDC) registryToken(w http.ResponseWriter, r *http.Request) strin
|
||||
return raw
|
||||
}
|
||||
|
||||
// maxTokenForm fits a large OIDC JWT plus the grant's other fields.
|
||||
const maxTokenForm = 16 << 10
|
||||
|
||||
func writeToken(w http.ResponseWriter, tok string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"token": tok, "access_token": tok})
|
||||
|
||||
@@ -5,12 +5,8 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,14 +14,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/core/remotes"
|
||||
"github.com/containerd/containerd/v2/core/remotes/docker"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
"github.com/opencontainers/go-digest"
|
||||
specs "github.com/opencontainers/image-spec/specs-go"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
||||
)
|
||||
@@ -330,47 +321,6 @@ func TestRequireRegistryWrites(t *testing.T) {
|
||||
t.Errorf("rejected credential %q: %d %s", authz, w.Code, w.Body)
|
||||
}
|
||||
}
|
||||
post := func(form url.Values) *httptest.ResponseRecorder {
|
||||
r := httptest.NewRequest(http.MethodPost, "/v2/token", strings.NewReader(form.Encode()))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
form url.Values
|
||||
want string
|
||||
}{
|
||||
{"password grant", url.Values{"grant_type": {"password"}, "username": {"ci"}, "password": {valid}, "service": {"artifactapi"}}, valid},
|
||||
{"refresh grant", url.Values{"grant_type": {"refresh_token"}, "refresh_token": {valid}}, valid},
|
||||
{"bogus password", url.Values{"grant_type": {"password"}, "username": {"ci"}, "password": {"bogus"}}, "anonymous"},
|
||||
{"expired refresh", url.Values{"grant_type": {"refresh_token"}, "refresh_token": {expired}}, "anonymous"},
|
||||
{"no credential", url.Values{"grant_type": {"password"}, "username": {"ci"}}, "anonymous"},
|
||||
} {
|
||||
if w := post(tc.form); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), `"access_token":"`+tc.want+`"`) {
|
||||
t.Errorf("oauth %s: %d %s", tc.name, w.Code, w.Body)
|
||||
}
|
||||
}
|
||||
var mp strings.Builder
|
||||
mw := multipart.NewWriter(&mp)
|
||||
_ = mw.WriteField("password", valid)
|
||||
_ = mw.Close()
|
||||
r := httptest.NewRequest(http.MethodPost, "/v2/token", strings.NewReader(mp.String()))
|
||||
r.Header.Set("Content-Type", mw.FormDataContentType())
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if !strings.Contains(w.Body.String(), `"token":"anonymous"`) || r.MultipartForm != nil {
|
||||
t.Errorf("multipart form honoured: %s", w.Body)
|
||||
}
|
||||
big := &countingReader{r: io.MultiReader(strings.NewReader("password="+valid+"&pad="), strings.NewReader(strings.Repeat("a", 1<<20)))}
|
||||
r = httptest.NewRequest(http.MethodPost, "/v2/token", big)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if !strings.Contains(w.Body.String(), `"token":"anonymous"`) || big.n > 2*maxTokenForm {
|
||||
t.Errorf("oversized form: read %d bytes, %s", big.n, w.Body)
|
||||
}
|
||||
for _, m := range []string{http.MethodGet, http.MethodHead} {
|
||||
if w := do(m, "/v2/repo/img/manifests/t", ""); w.Code != http.StatusNoContent {
|
||||
t.Errorf("anonymous %s: %d", m, w.Code)
|
||||
@@ -391,120 +341,3 @@ func TestRequireRegistryWrites(t *testing.T) {
|
||||
t.Errorf("disabled: status %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// stubRegistry is just enough of a Registry V2 for containerd to push a blob
|
||||
// and manifest and resolve/fetch them back.
|
||||
func stubRegistry() http.Handler {
|
||||
var mu sync.Mutex
|
||||
store := map[string][]byte{}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
p := r.URL.Path
|
||||
switch {
|
||||
case r.Method == http.MethodPost && strings.HasSuffix(p, "/blobs/uploads/"):
|
||||
w.Header().Set("Location", p+"u1")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
case r.Method == http.MethodPut && strings.Contains(p, "/blobs/uploads/"):
|
||||
dgst := r.URL.Query().Get("digest")
|
||||
store[strings.SplitN(p, "/blobs/", 2)[0]+"/blobs/"+dgst], _ = io.ReadAll(r.Body)
|
||||
w.Header().Set("Docker-Content-Digest", dgst)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
case r.Method == http.MethodPut:
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
store[p] = body
|
||||
store[strings.SplitN(p, "/manifests/", 2)[0]+"/manifests/"+digest.FromBytes(body).String()] = body
|
||||
w.Header().Set("Docker-Content-Digest", digest.FromBytes(body).String())
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
default:
|
||||
body, found := store[p]
|
||||
if !found {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if strings.Contains(p, "/manifests/") {
|
||||
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
|
||||
}
|
||||
w.Header().Set("Docker-Content-Digest", digest.FromBytes(body).String())
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRegistryContainerdClient drives the real containerd resolver (used by
|
||||
// buildkit), which fetches tokens with an OAuth2 POST form.
|
||||
func TestRegistryContainerdClient(t *testing.T) {
|
||||
iss := testsupport.NewOIDCIssuer()
|
||||
defer iss.Close()
|
||||
var srv *httptest.Server
|
||||
srv = httptest.NewServer(NewBearerOIDC(iss.URL, []string{"artifactapi"}).RequireRegistryWrites(stubRegistry(), func(*http.Request) string { return srv.URL + "/v2/token" }))
|
||||
defer srv.Close()
|
||||
ref := strings.TrimPrefix(srv.URL, "http://") + "/repo/img:t"
|
||||
ctx := context.Background()
|
||||
|
||||
resolver := func(secret string) remotes.Resolver {
|
||||
return docker.NewResolver(docker.ResolverOptions{Hosts: docker.ConfigureDefaultRegistries(
|
||||
docker.WithPlainHTTP(docker.MatchAllHosts),
|
||||
docker.WithAuthorizer(docker.NewDockerAuthorizer(docker.WithAuthCreds(func(string) (string, string, error) { return "ci", secret, nil }))),
|
||||
)})
|
||||
}
|
||||
push := func(res remotes.Resolver, mediaType string, data []byte) (ocispec.Descriptor, error) {
|
||||
desc := ocispec.Descriptor{MediaType: mediaType, Digest: digest.FromBytes(data), Size: int64(len(data))}
|
||||
p, err := res.Pusher(ctx, ref)
|
||||
if err != nil {
|
||||
return desc, err
|
||||
}
|
||||
cw, err := p.Push(ctx, desc)
|
||||
if err != nil {
|
||||
return desc, err
|
||||
}
|
||||
defer cw.Close()
|
||||
if _, err := cw.Write(data); err != nil {
|
||||
return desc, err
|
||||
}
|
||||
return desc, cw.Commit(ctx, desc.Size, desc.Digest)
|
||||
}
|
||||
pushImage := func(res remotes.Resolver, arch string) error {
|
||||
cfg, err := push(res, ocispec.MediaTypeImageConfig, []byte(`{"architecture":"`+arch+`","os":"linux"}`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
manifest, _ := json.Marshal(ocispec.Manifest{Versioned: specs.Versioned{SchemaVersion: 2}, MediaType: ocispec.MediaTypeImageManifest, Config: cfg, Layers: []ocispec.Descriptor{}})
|
||||
_, err = push(res, ocispec.MediaTypeImageManifest, manifest)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pushImage(resolver(iss.Token(nil, "artifactapi", "ci", time.Hour)), "amd64"); err != nil {
|
||||
t.Fatalf("push with valid credential: %v", err)
|
||||
}
|
||||
for name, secret := range map[string]string{"bogus": "bogus", "expired": iss.Token(nil, "artifactapi", "ci", -time.Minute)} {
|
||||
if err := pushImage(resolver(secret), name); err == nil || !strings.Contains(err.Error(), "401") {
|
||||
t.Errorf("push with %s credential: want 401, got %v", name, err)
|
||||
}
|
||||
res := resolver(secret)
|
||||
_, desc, err := res.Resolve(ctx, ref)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve with %s credential: %v", name, err)
|
||||
}
|
||||
f, _ := res.Fetcher(ctx, ref)
|
||||
rc, err := f.Fetch(ctx, desc)
|
||||
if err != nil {
|
||||
t.Fatalf("pull with %s credential: %v", name, err)
|
||||
}
|
||||
_ = rc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type countingReader struct {
|
||||
r io.Reader
|
||||
n int
|
||||
}
|
||||
|
||||
func (c *countingReader) Read(p []byte) (int, error) {
|
||||
n, err := c.r.Read(p)
|
||||
c.n += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "==> building and starting stack (postgres, redis, minio, mockupstream(s), artifactapi)"
|
||||
"${COMPOSE[@]}" up -d --build postgres redis minio mockupstream mockupstreama mockupstreamb artifactapi
|
||||
echo "==> building and starting stack (postgres, redis, minio, mockupstream(s), mockoidc, artifactapi)"
|
||||
"${COMPOSE[@]}" up -d --build postgres redis minio mockupstream mockupstreama mockupstreamb mockoidc artifactapi
|
||||
|
||||
echo "==> waiting for artifactapi health at ${API_URL}"
|
||||
for i in $(seq 1 60); do
|
||||
|
||||
Reference in New Issue
Block a user