mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-08-06 08:54:21 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3618385b28 | ||
|
|
55a625f733 |
@@ -85,7 +85,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -6,7 +6,7 @@ SHASUM ?= shasum -a 256
|
|||||||
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
|
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
|
||||||
XGO_VERSION := go-1.26.x
|
XGO_VERSION := go-1.26.x
|
||||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go
|
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.16 # renovate: datasource=go
|
||||||
|
|
||||||
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
LINUX_ARCHS ?= linux/amd64,linux/arm64
|
||||||
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
DARWIN_ARCHS ?= darwin-12/amd64,darwin-12/arm64
|
||||||
@@ -19,7 +19,7 @@ DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
|||||||
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless
|
||||||
|
|
||||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # renovate: datasource=go
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.6.0 # renovate: datasource=go
|
||||||
|
|
||||||
GOTEST_FLAGS ?= -race -timeout 20m -parallel 8
|
GOTEST_FLAGS ?= -race -timeout 20m -parallel 8
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ type credKey struct{}
|
|||||||
// poison another repo's cache, even from inside a container that reaches the
|
// poison another repo's cache, even from inside a container that reaches the
|
||||||
// cache server over the docker bridge network.
|
// cache server over the docker bridge network.
|
||||||
type JobCredential struct {
|
type JobCredential struct {
|
||||||
Repo string
|
Repo string `json:"repo"`
|
||||||
|
|
||||||
|
// Results is the instance whose artifact service this server forwards for the job, and
|
||||||
|
// InsecureTLS how the runner reaches it; see results.go. The tags are the wire format a
|
||||||
|
// remote runner registers with.
|
||||||
|
Results string `json:"results"`
|
||||||
|
InsecureTLS bool `json:"insecure_tls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// credEntry holds a registered job's credential along with an active
|
// credEntry holds a registered job's credential along with an active
|
||||||
@@ -165,6 +171,7 @@ func StartHandler(dir, outboundIP string, port uint16, internalSecret string, lo
|
|||||||
router.POST(internalPath+"/register", h.internalAuth(h.internalRegister))
|
router.POST(internalPath+"/register", h.internalAuth(h.internalRegister))
|
||||||
router.POST(internalPath+"/revoke", h.internalAuth(h.internalRevoke))
|
router.POST(internalPath+"/revoke", h.internalAuth(h.internalRevoke))
|
||||||
h.registerV2Routes(router)
|
h.registerV2Routes(router)
|
||||||
|
router.NotFound = http.HandlerFunc(h.forwardOrNotFound)
|
||||||
|
|
||||||
h.router = router
|
h.router = router
|
||||||
|
|
||||||
@@ -211,10 +218,11 @@ func (h *Handler) ExternalURL() string {
|
|||||||
// is only accepted while the job is running.
|
// is only accepted while the job is running.
|
||||||
//
|
//
|
||||||
// Registrations are reference-counted: if a token is already registered, the
|
// Registrations are reference-counted: if a token is already registered, the
|
||||||
// existing repo is kept and the refcount is incremented. The entry is
|
// credential it was registered with is kept and the refcount is incremented.
|
||||||
// removed only when every revoker returned by RegisterJob has been called.
|
// The entry is removed only when every revoker returned by RegisterJob has
|
||||||
|
// been called.
|
||||||
// This keeps a stray re-registration from silently revoking a live job.
|
// This keeps a stray re-registration from silently revoking a live job.
|
||||||
func (h *Handler) RegisterJob(token, repo string) func() {
|
func (h *Handler) RegisterJob(token string, cred JobCredential) func() {
|
||||||
if h == nil || token == "" {
|
if h == nil || token == "" {
|
||||||
return func() {}
|
return func() {}
|
||||||
}
|
}
|
||||||
@@ -223,7 +231,7 @@ func (h *Handler) RegisterJob(token, repo string) func() {
|
|||||||
existing.refs++
|
existing.refs++
|
||||||
} else {
|
} else {
|
||||||
h.creds[token] = &credEntry{
|
h.creds[token] = &credEntry{
|
||||||
cred: JobCredential{Repo: repo},
|
cred: cred,
|
||||||
refs: 1,
|
refs: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -619,7 +627,7 @@ func (h *Handler) internalAuth(handler httprouter.Handle) httprouter.Handle {
|
|||||||
|
|
||||||
type internalRegisterBody struct {
|
type internalRegisterBody struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Repo string `json:"repo"`
|
JobCredential
|
||||||
}
|
}
|
||||||
|
|
||||||
type internalRevokeBody struct {
|
type internalRevokeBody struct {
|
||||||
@@ -627,6 +635,15 @@ type internalRevokeBody struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST /_internal/register
|
// POST /_internal/register
|
||||||
|
// ResultsURL is what a job registered with cred should be given as ACTIONS_RESULTS_URL, or "" when
|
||||||
|
// the credential names no instance to forward the artifact half to.
|
||||||
|
func (h *Handler) ResultsURL(cred JobCredential) string {
|
||||||
|
if h == nil || cred.Results == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return h.ExternalURL()
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) internalRegister(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
func (h *Handler) internalRegister(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
var body internalRegisterBody
|
var body internalRegisterBody
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
@@ -637,8 +654,9 @@ func (h *Handler) internalRegister(w http.ResponseWriter, r *http.Request, _ htt
|
|||||||
h.responseJSON(w, r, http.StatusBadRequest, errors.New("token is required"))
|
h.responseJSON(w, r, http.StatusBadRequest, errors.New("token is required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.RegisterJob(body.Token, body.Repo)
|
h.RegisterJob(body.Token, body.JobCredential)
|
||||||
h.responseJSON(w, r, http.StatusOK)
|
// A server too old to forward answers without this, which is how the caller knows.
|
||||||
|
h.responseJSON(w, r, http.StatusOK, map[string]any{"results_url": h.ResultsURL(body.JobCredential)})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /_internal/revoke
|
// POST /_internal/revoke
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func TestHandler(t *testing.T) {
|
|||||||
dir := filepath.Join(t.TempDir(), "artifactcache")
|
dir := filepath.Join(t.TempDir(), "artifactcache")
|
||||||
handler, err := StartHandler(dir, "", 0, "", nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
handler.RegisterJob(testToken, testRepo)
|
handler.RegisterJob(testToken, JobCredential{Repo: testRepo})
|
||||||
|
|
||||||
base := fmt.Sprintf("%s%s", handler.ExternalURL(), apiPath)
|
base := fmt.Sprintf("%s%s", handler.ExternalURL(), apiPath)
|
||||||
|
|
||||||
@@ -890,7 +890,7 @@ func TestHandler_UnregisterRevokes(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
|
|
||||||
unregister := handler.RegisterJob("tmp-token", testRepo)
|
unregister := handler.RegisterJob("tmp-token", JobCredential{Repo: testRepo})
|
||||||
|
|
||||||
base := handler.ExternalURL() + apiPath
|
base := handler.ExternalURL() + apiPath
|
||||||
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=y", nil)
|
req, err := http.NewRequest(http.MethodGet, base+"/cache?keys=x&version=y", nil)
|
||||||
@@ -920,8 +920,8 @@ func TestHandler_CrossRepoIsolation(t *testing.T) {
|
|||||||
handler, err := StartHandler(dir, "", 0, "", nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
handler.RegisterJob("token-a", "owner/repoA")
|
handler.RegisterJob("token-a", JobCredential{Repo: "owner/repoA"})
|
||||||
handler.RegisterJob("token-b", "owner/repoB")
|
handler.RegisterJob("token-b", JobCredential{Repo: "owner/repoB"})
|
||||||
|
|
||||||
base := handler.ExternalURL() + apiPath
|
base := handler.ExternalURL() + apiPath
|
||||||
key := "shared-key"
|
key := "shared-key"
|
||||||
@@ -986,7 +986,7 @@ func TestHandler_ArtifactSignature(t *testing.T) {
|
|||||||
handler, err := StartHandler(dir, "", 0, "", nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
handler.RegisterJob(testToken, testRepo)
|
handler.RegisterJob(testToken, JobCredential{Repo: testRepo})
|
||||||
|
|
||||||
base := handler.ExternalURL() + apiPath
|
base := handler.ExternalURL() + apiPath
|
||||||
|
|
||||||
@@ -1059,7 +1059,7 @@ func TestHandler_ArtifactSignatureDownload(t *testing.T) {
|
|||||||
handler, err := StartHandler(dir, "", 0, "", nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
handler.RegisterJob(testToken, testRepo)
|
handler.RegisterJob(testToken, JobCredential{Repo: testRepo})
|
||||||
|
|
||||||
base := handler.ExternalURL() + apiPath
|
base := handler.ExternalURL() + apiPath
|
||||||
key := "download-key"
|
key := "download-key"
|
||||||
@@ -1100,8 +1100,8 @@ func TestHandler_RegisterJob_RefCounted(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
|
|
||||||
first := handler.RegisterJob("shared", testRepo)
|
first := handler.RegisterJob("shared", JobCredential{Repo: testRepo})
|
||||||
second := handler.RegisterJob("shared", testRepo)
|
second := handler.RegisterJob("shared", JobCredential{Repo: testRepo})
|
||||||
|
|
||||||
base := handler.ExternalURL() + apiPath
|
base := handler.ExternalURL() + apiPath
|
||||||
probe := func() int {
|
probe := func() int {
|
||||||
@@ -1131,8 +1131,8 @@ func TestHandler_GC_PerRepoDedup(t *testing.T) {
|
|||||||
handler, err := StartHandler(dir, "", 0, "", nil)
|
handler, err := StartHandler(dir, "", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
handler.RegisterJob("tok-a", "owner/repoA")
|
handler.RegisterJob("tok-a", JobCredential{Repo: "owner/repoA"})
|
||||||
handler.RegisterJob("tok-b", "owner/repoB")
|
handler.RegisterJob("tok-b", JobCredential{Repo: "owner/repoB"})
|
||||||
|
|
||||||
key := "shared-dedup-key"
|
key := "shared-dedup-key"
|
||||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ import (
|
|||||||
// endpoints, and uploads the archive to the returned URL with the Azure blob protocol.
|
// endpoints, and uploads the archive to the returned URL with the Azure blob protocol.
|
||||||
// Both API versions are served from the same store, so a repository keeps its cache
|
// Both API versions are served from the same store, so a repository keeps its cache
|
||||||
// when a workflow moves between action versions.
|
// when a workflow moves between action versions.
|
||||||
|
//
|
||||||
|
// Responses carry the proto field names, which is what Gitea's own results API emits and the only
|
||||||
|
// spelling the Go clients parse. The JavaScript toolkit accepts either.
|
||||||
const (
|
const (
|
||||||
cacheServiceV2Path = "/twirp/github.actions.results.api.v1.CacheService"
|
cacheServiceV2Path = "/twirp/github.actions.results.api.v1.CacheService"
|
||||||
|
|
||||||
@@ -94,7 +97,7 @@ func (h *Handler) v2CreateCacheEntry(w http.ResponseWriter, r *http.Request, _ h
|
|||||||
|
|
||||||
h.responseJSON(w, r, http.StatusOK, map[string]any{
|
h.responseJSON(w, r, http.StatusOK, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"signedUploadUrl": h.signedURL(blobPath, blobUploadPurpose, cache.ID, time.Now().Add(blobUploadURLTTL)),
|
"signed_upload_url": h.signedURL(blobPath, blobUploadPurpose, cache.ID, time.Now().Add(blobUploadURLTTL)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@ func (h *Handler) v2FinalizeCacheEntryUpload(w http.ResponseWriter, r *http.Requ
|
|||||||
h.responseJSON(w, r, http.StatusOK, map[string]any{
|
h.responseJSON(w, r, http.StatusOK, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
// int64 fields travel as strings in the proto JSON mapping.
|
// int64 fields travel as strings in the proto JSON mapping.
|
||||||
"entryId": strconv.FormatUint(cache.ID, 10),
|
"entry_id": strconv.FormatUint(cache.ID, 10),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,8 +168,8 @@ func (h *Handler) v2GetCacheEntryDownloadURL(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
h.responseJSON(w, r, http.StatusOK, map[string]any{
|
h.responseJSON(w, r, http.StatusOK, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"signedDownloadUrl": h.signedArtifactURL(cache.ID, time.Now().Add(artifactURLTTL)),
|
"signed_download_url": h.signedArtifactURL(cache.ID, time.Now().Add(artifactURLTTL)),
|
||||||
"matchedKey": cache.Key,
|
"matched_key": cache.Key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +212,8 @@ func (h *Handler) v2UploadBlob(w http.ResponseWriter, r *http.Request, params ht
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Azure SDK client dereferences this without checking, so its absence panics the caller.
|
||||||
|
w.Header().Set("x-ms-request-id", strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ func putBlob(t *testing.T, url string, content []byte) int {
|
|||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == http.StatusCreated {
|
||||||
|
// The Azure SDK client dereferences this header without checking, so a blob upload that
|
||||||
|
// omits it panics the caller rather than failing it.
|
||||||
|
require.NotEmpty(t, resp.Header.Get("x-ms-request-id"))
|
||||||
|
}
|
||||||
return resp.StatusCode
|
return resp.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ func startTestHandler(t *testing.T) *Handler {
|
|||||||
handler, err := StartHandler(filepath.Join(t.TempDir(), "artifactcache"), "127.0.0.1", 0, "", nil)
|
handler, err := StartHandler(filepath.Join(t.TempDir(), "artifactcache"), "127.0.0.1", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() { _ = handler.Close() })
|
t.Cleanup(func() { _ = handler.Close() })
|
||||||
handler.RegisterJob(testToken, testRepo)
|
handler.RegisterJob(testToken, JobCredential{Repo: testRepo})
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ func saveV2(t *testing.T, handler *Handler, key, version string, content []byte)
|
|||||||
|
|
||||||
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": key, "version": version})
|
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": key, "version": version})
|
||||||
require.Equal(t, true, created["ok"])
|
require.Equal(t, true, created["ok"])
|
||||||
uploadURL, _ = created["signedUploadUrl"].(string)
|
uploadURL, _ = created["signed_upload_url"].(string)
|
||||||
require.NotEmpty(t, uploadURL)
|
require.NotEmpty(t, uploadURL)
|
||||||
require.Equal(t, http.StatusCreated, putBlob(t, uploadURL, content))
|
require.Equal(t, http.StatusCreated, putBlob(t, uploadURL, content))
|
||||||
|
|
||||||
@@ -100,7 +105,7 @@ func TestCacheServiceV2RoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
finalized, uploadURL := saveV2(t, handler, "deps-v1", "abc123", content)
|
finalized, uploadURL := saveV2(t, handler, "deps-v1", "abc123", content)
|
||||||
require.Equal(t, true, finalized["ok"])
|
require.Equal(t, true, finalized["ok"])
|
||||||
assert.NotEmpty(t, finalized["entryId"])
|
assert.NotEmpty(t, finalized["entry_id"])
|
||||||
|
|
||||||
// The upload URL outlives the finalize call, so replaying it must not poison the entry,
|
// The upload URL outlives the finalize call, so replaying it must not poison the entry,
|
||||||
// and it is an upload URL only: nothing reads a blob back through it.
|
// and it is an upload URL only: nothing reads a blob back through it.
|
||||||
@@ -112,8 +117,8 @@ func TestCacheServiceV2RoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
got := v2Call(t, handler, testClient, "GetCacheEntryDownloadURL", map[string]any{"key": "deps-v1", "version": "abc123"})
|
got := v2Call(t, handler, testClient, "GetCacheEntryDownloadURL", map[string]any{"key": "deps-v1", "version": "abc123"})
|
||||||
require.Equal(t, true, got["ok"])
|
require.Equal(t, true, got["ok"])
|
||||||
assert.Equal(t, "deps-v1", got["matchedKey"])
|
assert.Equal(t, "deps-v1", got["matched_key"])
|
||||||
downloadURL, _ := got["signedDownloadUrl"].(string)
|
downloadURL, _ := got["signed_download_url"].(string)
|
||||||
require.NotEmpty(t, downloadURL)
|
require.NotEmpty(t, downloadURL)
|
||||||
assert.Equal(t, content, getURL(t, downloadURL))
|
assert.Equal(t, content, getURL(t, downloadURL))
|
||||||
}
|
}
|
||||||
@@ -124,7 +129,7 @@ func TestCacheServiceV2BlockUpload(t *testing.T) {
|
|||||||
handler := startTestHandler(t)
|
handler := startTestHandler(t)
|
||||||
|
|
||||||
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "blocks", "version": "v1"})
|
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "blocks", "version": "v1"})
|
||||||
uploadURL, _ := created["signedUploadUrl"].(string)
|
uploadURL, _ := created["signed_upload_url"].(string)
|
||||||
require.NotEmpty(t, uploadURL)
|
require.NotEmpty(t, uploadURL)
|
||||||
|
|
||||||
blocks := map[string][]byte{}
|
blocks := map[string][]byte{}
|
||||||
@@ -154,7 +159,7 @@ func TestCacheServiceV2BlockUpload(t *testing.T) {
|
|||||||
|
|
||||||
got := v2Call(t, handler, testClient, "GetCacheEntryDownloadURL", map[string]any{"key": "blocks", "version": "v1"})
|
got := v2Call(t, handler, testClient, "GetCacheEntryDownloadURL", map[string]any{"key": "blocks", "version": "v1"})
|
||||||
require.Equal(t, true, got["ok"])
|
require.Equal(t, true, got["ok"])
|
||||||
assert.Equal(t, "hello world!", string(getURL(t, got["signedDownloadUrl"].(string))))
|
assert.Equal(t, "hello world!", string(getURL(t, got["signed_download_url"].(string))))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheServiceV2Lookups(t *testing.T) {
|
func TestCacheServiceV2Lookups(t *testing.T) {
|
||||||
@@ -175,7 +180,7 @@ func TestCacheServiceV2Lookups(t *testing.T) {
|
|||||||
"key": "deps-zzz", field: []string{"deps-"}, "version": "v1",
|
"key": "deps-zzz", field: []string{"deps-"}, "version": "v1",
|
||||||
})
|
})
|
||||||
require.Equal(t, true, got["ok"])
|
require.Equal(t, true, got["ok"])
|
||||||
assert.Equal(t, "deps-abc", got["matchedKey"])
|
assert.Equal(t, "deps-abc", got["matched_key"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +195,7 @@ func TestCacheServiceV2Lookups(t *testing.T) {
|
|||||||
t.Run("a prefix of an existing key is still reserved", func(t *testing.T) {
|
t.Run("a prefix of an existing key is still reserved", func(t *testing.T) {
|
||||||
reserved := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "deps", "version": "v1"})
|
reserved := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "deps", "version": "v1"})
|
||||||
require.Equal(t, true, reserved["ok"])
|
require.Equal(t, true, reserved["ok"])
|
||||||
assert.NotEmpty(t, reserved["signedUploadUrl"])
|
assert.NotEmpty(t, reserved["signed_upload_url"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("finalizing without a reservation is not ok", func(t *testing.T) {
|
t.Run("finalizing without a reservation is not ok", func(t *testing.T) {
|
||||||
@@ -203,7 +208,7 @@ func TestCacheServiceV2Lookups(t *testing.T) {
|
|||||||
// The size the client declares is what Commit validates the assembled archive against.
|
// The size the client declares is what Commit validates the assembled archive against.
|
||||||
t.Run("finalizing with the wrong size is not ok", func(t *testing.T) {
|
t.Run("finalizing with the wrong size is not ok", func(t *testing.T) {
|
||||||
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "wrong-size", "version": "v1"})
|
created := v2Call(t, handler, testClient, "CreateCacheEntry", map[string]any{"key": "wrong-size", "version": "v1"})
|
||||||
require.Equal(t, http.StatusCreated, putBlob(t, created["signedUploadUrl"].(string), []byte("four")))
|
require.Equal(t, http.StatusCreated, putBlob(t, created["signed_upload_url"].(string), []byte("four")))
|
||||||
|
|
||||||
got := v2Call(t, handler, testClient, "FinalizeCacheEntryUpload", map[string]any{
|
got := v2Call(t, handler, testClient, "FinalizeCacheEntryUpload", map[string]any{
|
||||||
"key": "wrong-size", "version": "v1", "size_bytes": 99,
|
"key": "wrong-size", "version": "v1", "size_bytes": 99,
|
||||||
@@ -227,7 +232,7 @@ func TestCacheServiceV2Lookups(t *testing.T) {
|
|||||||
|
|
||||||
// The cache of one repository must stay invisible to another, as it does for the v1 API.
|
// The cache of one repository must stay invisible to another, as it does for the v1 API.
|
||||||
t.Run("another repository sees nothing", func(t *testing.T) {
|
t.Run("another repository sees nothing", func(t *testing.T) {
|
||||||
handler.RegisterJob("other-runtime-token", "other/repo")
|
handler.RegisterJob("other-runtime-token", JobCredential{Repo: "other/repo"})
|
||||||
otherClient := &http.Client{Transport: &bearerTransport{token: "other-runtime-token"}}
|
otherClient := &http.Client{Transport: &bearerTransport{token: "other-runtime-token"}}
|
||||||
|
|
||||||
got := v2Call(t, handler, otherClient, "GetCacheEntryDownloadURL", map[string]any{"key": "deps-abc", "version": "v1"})
|
got := v2Call(t, handler, otherClient, "GetCacheEntryDownloadURL", map[string]any{"key": "deps-abc", "version": "v1"})
|
||||||
|
|||||||
59
act/artifactcache/results.go
Normal file
59
act/artifactcache/results.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package artifactcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The results service is one origin serving every github.actions.results.api.v1 service, and
|
||||||
|
// Gitea implements only the artifact half of it. Forwarding that half from here makes this origin
|
||||||
|
// the whole service, so ACTIONS_RESULTS_URL can point at it truthfully, which is what the clients
|
||||||
|
// this runner cannot patch need, docker buildx among them.
|
||||||
|
//
|
||||||
|
// The instance to forward to travels with the job registration rather than with configuration, so
|
||||||
|
// a cache server shared between runners serves each of their instances.
|
||||||
|
const artifactServicePath = "/twirp/github.actions.results.api.v1.ArtifactService/"
|
||||||
|
|
||||||
|
// forwardOrNotFound is the router's fallback: the artifact service of the instance the job
|
||||||
|
// registered with, and the 404 the router would have written otherwise.
|
||||||
|
func (h *Handler) forwardOrNotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cred, ok := h.lookupCredential(bearerToken(r))
|
||||||
|
if !ok || cred.Results == "" || !strings.HasPrefix(r.URL.Path, artifactServicePath) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target, err := url.Parse(strings.TrimSuffix(cred.Results, "/"))
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("artifact service forward to %q: %v", cred.Results, err)
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.logger.Debugf("%s %s: forwarding to %s", r.Method, r.URL.Path, target)
|
||||||
|
|
||||||
|
proxy := &httputil.ReverseProxy{
|
||||||
|
Rewrite: func(r *httputil.ProxyRequest) {
|
||||||
|
r.SetURL(target)
|
||||||
|
// Gitea builds the URLs it hands back from this Host, and their scheme from the
|
||||||
|
// connection unless a forwarded header overrides it, so artifact bodies go to Gitea
|
||||||
|
// directly and never through here.
|
||||||
|
r.Out.Host = target.Host
|
||||||
|
},
|
||||||
|
ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) {
|
||||||
|
h.logger.Warnf("artifact service forward to %s: %v", target, err)
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if cred.InsecureTLS {
|
||||||
|
proxy.Transport = insecureTransport
|
||||||
|
}
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insecureTransport is shared, because a transport per request would pool no connections.
|
||||||
|
var insecureTransport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec // the runner reaches its instance on the operator's say-so
|
||||||
56
act/artifactcache/results_test.go
Normal file
56
act/artifactcache/results_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package artifactcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The artifact half is forwarded under the Host Gitea knows itself by, so the URLs it hands back
|
||||||
|
// still point at Gitea, and nothing else is proxied.
|
||||||
|
func TestFrontResultsService(t *testing.T) {
|
||||||
|
var gotHost, gotPath, gotProto string
|
||||||
|
gitea := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotHost, gotPath, gotProto = r.Host, r.URL.Path, r.Header.Get("X-Forwarded-Proto")
|
||||||
|
_, _ = io.WriteString(w, `{"ok":true}`)
|
||||||
|
}))
|
||||||
|
defer gitea.Close()
|
||||||
|
|
||||||
|
handler, err := StartHandler(t.TempDir(), "127.0.0.1", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer handler.Close()
|
||||||
|
const token = "forward-token"
|
||||||
|
|
||||||
|
client := &http.Client{Transport: &bearerTransport{token: token}}
|
||||||
|
post := func(path string) int {
|
||||||
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, handler.ExternalURL()+path, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp.Body.Close()
|
||||||
|
return resp.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, post(artifactServicePath+"CreateArtifact"),
|
||||||
|
"an unregistered token is forwarded nowhere")
|
||||||
|
|
||||||
|
defer handler.RegisterJob(token, JobCredential{Repo: "owner/repo", Results: gitea.URL})()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, post(artifactServicePath+"CreateArtifact"))
|
||||||
|
assert.Equal(t, strings.TrimPrefix(gitea.URL, "http://"), gotHost, "Gitea must see the host it mints its URLs from")
|
||||||
|
assert.Empty(t, gotProto, "a forwarded scheme would make an https Gitea mint http URLs")
|
||||||
|
assert.Equal(t, artifactServicePath+"CreateArtifact", gotPath)
|
||||||
|
|
||||||
|
gotPath = ""
|
||||||
|
assert.Equal(t, http.StatusNotFound, post("/twirp/github.actions.results.api.v1.OtherService/Do"))
|
||||||
|
assert.Equal(t, http.StatusNotFound, post("/api/v1/repos/owner/repo"))
|
||||||
|
assert.Empty(t, gotPath, "only the artifact service is forwarded")
|
||||||
|
}
|
||||||
@@ -74,6 +74,7 @@ type Config struct {
|
|||||||
ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options
|
ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options
|
||||||
ActionCache ActionCache // Use a custom ActionCache Implementation
|
ActionCache ActionCache // Use a custom ActionCache Implementation
|
||||||
ProxyEnv map[string]string // the proxy variables the job runs with, also given to service containers and image builds
|
ProxyEnv map[string]string // the proxy variables the job runs with, also given to service containers and image builds
|
||||||
|
PatchToolkit bool // edit the @actions toolkit bundled into an action so it works against Gitea, see toolkit_patch.go
|
||||||
|
|
||||||
PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc.
|
PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc.
|
||||||
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
|
EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath
|
||||||
|
|||||||
@@ -236,10 +236,10 @@ func (sar *stepActionRemote) toolkitBundles() (string, []string) {
|
|||||||
return dir, actionScriptPaths(filepath.Join(dir, sar.remoteAction.Path), sar.action)
|
return dir, actionScriptPaths(filepath.Join(dir, sar.remoteAction.Path), sar.action)
|
||||||
}
|
}
|
||||||
|
|
||||||
// patchActionToolkit edits the bundled toolkit so it works against Gitea, which lets the cache
|
// patchActionToolkit edits the bundled toolkit so it works against Gitea: the artifact actions
|
||||||
// client use the v2 API this runner serves. A no-op unless the runner serves it.
|
// stop refusing, and the cache client keeps to the cache server whichever API version it picks.
|
||||||
func (sar *stepActionRemote) patchActionToolkit(ctx context.Context) error {
|
func (sar *stepActionRemote) patchActionToolkit(ctx context.Context) error {
|
||||||
if sar.RunContext.GetEnv()[CacheServiceV2Env] != "" {
|
if sar.RunContext.Config.PatchToolkit {
|
||||||
dir, scripts := sar.toolkitBundles()
|
dir, scripts := sar.toolkitBundles()
|
||||||
patchToolkit(ctx, dir, scripts)
|
patchToolkit(ctx, dir, scripts)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -59,41 +63,65 @@ func bundleFromGitHub(t *testing.T, repo, ref, path string) string {
|
|||||||
return bundle
|
return bundle
|
||||||
}
|
}
|
||||||
|
|
||||||
// runCacheAction runs one entrypoint the way a job would: a real Gitea server URL, and a results
|
// jobEnv is the environment a job gets from this runner, which is what decides where an action's
|
||||||
// URL that points at Gitea rather than at the runner. Nothing about the environment is rewritten,
|
// toolkit looks for the cache and artifact services.
|
||||||
// so only the patch can make the client choose v2 and find the cache server.
|
type jobEnv struct {
|
||||||
func runCacheAction(t *testing.T, script, workspace, runnerTemp, cacheURL, token, key string) string {
|
workspace, runnerTemp string
|
||||||
|
cacheURL, resultsURL string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
// runActionEntrypoint runs one action entrypoint the way a job would. Adding another action to these tests
|
||||||
|
// means downloading its entrypoint with bundleFromGitHub and calling this with its inputs, whose
|
||||||
|
// names are the ones the action's own action.yml uses.
|
||||||
|
func runActionEntrypoint(t *testing.T, script string, env jobEnv, inputs map[string]string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
state := filepath.Join(runnerTemp, "state")
|
state := filepath.Join(env.runnerTemp, "state")
|
||||||
output := filepath.Join(runnerTemp, "output")
|
output := filepath.Join(env.runnerTemp, "output")
|
||||||
for _, name := range []string{state, output} {
|
for _, name := range []string{state, output} {
|
||||||
require.NoError(t, os.WriteFile(name, nil, 0o600))
|
require.NoError(t, os.WriteFile(name, nil, 0o600))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.CommandContext(t.Context(), "node", script)
|
cmd := exec.CommandContext(t.Context(), "node", script)
|
||||||
cmd.Dir = workspace
|
cmd.Dir = env.workspace
|
||||||
cmd.Env = append(os.Environ(),
|
cmd.Env = append(os.Environ(),
|
||||||
"INPUT_PATH=to-cache",
|
"ACTIONS_RUNTIME_TOKEN="+env.token,
|
||||||
"INPUT_KEY="+key,
|
"ACTIONS_CACHE_URL="+env.cacheURL+"/",
|
||||||
"ACTIONS_RUNTIME_TOKEN="+token,
|
"ACTIONS_RESULTS_URL="+env.resultsURL,
|
||||||
"ACTIONS_CACHE_URL="+cacheURL+"/",
|
|
||||||
// Unreachable on purpose: the artifact service lives here, the cache service must not.
|
|
||||||
"ACTIONS_RESULTS_URL=https://gitea.example",
|
|
||||||
"ACTIONS_CACHE_SERVICE_V2=true",
|
"ACTIONS_CACHE_SERVICE_V2=true",
|
||||||
"GITHUB_SERVER_URL=https://gitea.example.com",
|
"GITHUB_SERVER_URL=https://gitea.example.com",
|
||||||
|
"GITHUB_REPOSITORY=testuser/testrepo",
|
||||||
|
"GITHUB_RUN_ID=1",
|
||||||
"GITHUB_REF=refs/heads/main",
|
"GITHUB_REF=refs/heads/main",
|
||||||
"GITHUB_EVENT_NAME=push",
|
"GITHUB_EVENT_NAME=push",
|
||||||
"GITHUB_WORKSPACE="+workspace,
|
"GITHUB_WORKSPACE="+env.workspace,
|
||||||
"RUNNER_TEMP="+runnerTemp,
|
"RUNNER_TEMP="+env.runnerTemp,
|
||||||
"GITHUB_STATE="+state,
|
"GITHUB_STATE="+state,
|
||||||
"GITHUB_OUTPUT="+output,
|
"GITHUB_OUTPUT="+output,
|
||||||
)
|
)
|
||||||
|
for name, value := range inputs {
|
||||||
|
cmd.Env = append(cmd.Env, "INPUT_"+strings.ToUpper(name)+"="+value)
|
||||||
|
}
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
t.Logf("%s:\n%s", filepath.Base(filepath.Dir(script)), out)
|
t.Logf("%s:\n%s", filepath.Base(script), out)
|
||||||
require.NoError(t, err, "%s failed", script)
|
require.NoError(t, err, "%s failed", script)
|
||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// patchedAction downloads one entrypoint and patches it exactly as a downloaded action would be,
|
||||||
|
// keeping the untouched original in the sidecar beside it.
|
||||||
|
func patchedAction(t *testing.T, repo, ref, entrypoint string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
body, err := os.ReadFile(bundleFromGitHub(t, repo, ref, entrypoint))
|
||||||
|
require.NoError(t, err)
|
||||||
|
dir := tempDirPath(t)
|
||||||
|
script := filepath.Join(dir, filepath.Base(entrypoint))
|
||||||
|
require.NoError(t, os.WriteFile(script, body, 0o600))
|
||||||
|
patchToolkit(t.Context(), dir, []string{script})
|
||||||
|
return script
|
||||||
|
}
|
||||||
|
|
||||||
// tempDirPath is TempDir with symlinks resolved, because macOS hands out /var paths that resolve
|
// tempDirPath is TempDir with symlinks resolved, because macOS hands out /var paths that resolve
|
||||||
// to /private/var and the client derives archive paths relative to the workspace.
|
// to /private/var and the client derives archive paths relative to the workspace.
|
||||||
func tempDirPath(t *testing.T) string {
|
func tempDirPath(t *testing.T) string {
|
||||||
@@ -113,43 +141,52 @@ func tempDirPath(t *testing.T) string {
|
|||||||
func TestCacheServiceV2EndToEnd(t *testing.T) {
|
func TestCacheServiceV2EndToEnd(t *testing.T) {
|
||||||
requireHostTools(t, "node")
|
requireHostTools(t, "node")
|
||||||
|
|
||||||
// A stand-in action directory, patched exactly as a downloaded one would be.
|
restore := patchedAction(t, "actions/cache", actionsCacheRef, "dist/restore/index.js")
|
||||||
actionDir := tempDirPath(t)
|
save := patchedAction(t, "actions/cache", actionsCacheRef, "dist/save/index.js")
|
||||||
scripts := map[string]string{}
|
|
||||||
for _, stage := range []string{"restore", "save"} {
|
|
||||||
body, err := os.ReadFile(bundleFromGitHub(t, "actions/cache", actionsCacheRef, "dist/"+stage+"/index.js"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
scripts[stage] = filepath.Join(actionDir, stage+".js")
|
|
||||||
require.NoError(t, os.WriteFile(scripts[stage], body, 0o600))
|
|
||||||
}
|
|
||||||
patchToolkit(t.Context(), actionDir, []string{scripts["restore"], scripts["save"]})
|
|
||||||
|
|
||||||
handler, err := artifactcache.StartHandler(filepath.Join(t.TempDir(), "cache"), "127.0.0.1", 0, "", nil)
|
handler, err := artifactcache.StartHandler(filepath.Join(t.TempDir(), "cache"), "127.0.0.1", 0, "", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() { _ = handler.Close() })
|
t.Cleanup(func() { _ = handler.Close() })
|
||||||
const token, repo = "e2e-runtime-token", "testuser/testrepo"
|
const token, repo = "e2e-runtime-token", "testuser/testrepo"
|
||||||
handler.RegisterJob(token, repo)
|
handler.RegisterJob(token, artifactcache.JobCredential{Repo: repo})
|
||||||
|
|
||||||
workspace, runnerTemp := tempDirPath(t), tempDirPath(t)
|
env := jobEnv{
|
||||||
require.NoError(t, os.MkdirAll(filepath.Join(workspace, "to-cache"), 0o755))
|
workspace: tempDirPath(t),
|
||||||
|
runnerTemp: tempDirPath(t),
|
||||||
|
cacheURL: handler.ExternalURL(),
|
||||||
|
// The results service is the cache server's too, which is what the runner advertises.
|
||||||
|
resultsURL: handler.ExternalURL(),
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Join(env.workspace, "to-cache"), 0o755))
|
||||||
content := []byte("cached through the patched gate")
|
content := []byte("cached through the patched gate")
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(workspace, "to-cache", "data.txt"), content, 0o600))
|
require.NoError(t, os.WriteFile(filepath.Join(env.workspace, "to-cache", "data.txt"), content, 0o600))
|
||||||
|
|
||||||
const key = "patched-gate-key"
|
const key = "patched-gate-key"
|
||||||
missed := runCacheAction(t, scripts["restore"], workspace, runnerTemp, handler.ExternalURL(), token, key)
|
inputs := map[string]string{"path": "to-cache", "key": key}
|
||||||
|
|
||||||
|
missed := runActionEntrypoint(t, restore, env, inputs)
|
||||||
require.Contains(t, missed, "Cache service version: v2", "the patch did not take, the client stayed on v1")
|
require.Contains(t, missed, "Cache service version: v2", "the patch did not take, the client stayed on v1")
|
||||||
require.Contains(t, missed, "Cache not found for input keys: "+key)
|
require.Contains(t, missed, "Cache not found for input keys: "+key)
|
||||||
|
|
||||||
saved := runCacheAction(t, scripts["save"], workspace, runnerTemp, handler.ExternalURL(), token, key)
|
saved := runActionEntrypoint(t, save, env, inputs)
|
||||||
require.Contains(t, saved, "Cache saved with key: "+key)
|
require.Contains(t, saved, "Cache saved with key: "+key)
|
||||||
|
|
||||||
restored := tempDirPath(t)
|
env.workspace = tempDirPath(t)
|
||||||
hit := runCacheAction(t, scripts["restore"], restored, runnerTemp, handler.ExternalURL(), token, key)
|
hit := runActionEntrypoint(t, restore, env, inputs)
|
||||||
require.Contains(t, hit, "Cache restored from key: "+key)
|
require.Contains(t, hit, "Cache restored from key: "+key)
|
||||||
|
|
||||||
got, err := os.ReadFile(filepath.Join(restored, "to-cache", "data.txt"))
|
got, err := os.ReadFile(filepath.Join(env.workspace, "to-cache", "data.txt"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, content, got)
|
assert.Equal(t, content, got)
|
||||||
|
|
||||||
|
// Untouched, the same client takes a Gitea host for GHES and stays on v1, which reaches the
|
||||||
|
// cache server on its own address. That is what a runner without a results service of its own
|
||||||
|
// leaves its jobs with, so it has to round trip too.
|
||||||
|
env.workspace = tempDirPath(t)
|
||||||
|
v1 := runActionEntrypoint(t, filepath.Join(sidecarDir(filepath.Dir(restore)), "index.js"), env, inputs)
|
||||||
|
require.Contains(t, v1, "Cache service version: v1")
|
||||||
|
require.Contains(t, v1, "Cache restored from key: "+key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The gate and the URL getter are separate functions, and a bundler may put either first: the gap
|
// The gate and the URL getter are separate functions, and a bundler may put either first: the gap
|
||||||
@@ -158,19 +195,32 @@ func TestCacheServiceV2EndToEnd(t *testing.T) {
|
|||||||
// toolkit, patched but not run, is what keeps a future release from quietly matching only one of
|
// toolkit, patched but not run, is what keeps a future release from quietly matching only one of
|
||||||
// the two shapes and leaving every cache on v1.
|
// the two shapes and leaving every cache on v1.
|
||||||
func TestToolkitPatchAcrossActions(t *testing.T) {
|
func TestToolkitPatchAcrossActions(t *testing.T) {
|
||||||
for _, tc := range []struct{ repo, ref, path string }{
|
for _, tc := range []struct {
|
||||||
{"actions/setup-go", "v7.0.0", "dist/setup/index.js"},
|
repo, ref, path string
|
||||||
{"actions/setup-node", "v6.0.0", "dist/cache-save/index.js"},
|
wantPatched bool
|
||||||
{"actions/setup-python", "v6.0.0", "dist/setup/index.js"},
|
}{
|
||||||
{"ruby/setup-ruby", "v1.271.0", "dist/index.js"},
|
// The cache toolkit, in each bundler shape and from a spread of ecosystems, including the
|
||||||
{"pnpm/action-setup", "v6.0.9", "dist/index.js"},
|
// actions that drive a Go or a Rust cache client of their own.
|
||||||
|
{"actions/cache", actionsCacheRef, "dist/restore/index.js", true},
|
||||||
|
{"actions/setup-node", "v7.0.0", "dist/cache-save/index.js", true},
|
||||||
|
{"actions/setup-python", "v7.0.0", "dist/setup/index.js", true},
|
||||||
|
{"actions/setup-go", "v7.0.0", "dist/setup/index.js", true},
|
||||||
|
{"actions/setup-java", "v5.7.0", "dist/setup/index.js", true},
|
||||||
|
{"ruby/setup-ruby", "v1.321.0", "dist/index.js", true},
|
||||||
|
{"pnpm/action-setup", "v6.0.9", "dist/index.js", true},
|
||||||
|
{"oven-sh/setup-bun", "v2.2.0", "dist/setup/index.js", true},
|
||||||
|
{"Swatinem/rust-cache", "v2.9.1", "dist/restore/index.js", true},
|
||||||
|
{"docker/build-push-action", "v7.3.0", "dist/index.cjs", true},
|
||||||
// The artifact toolkit, where the gate is a refusal and there is nothing to redirect.
|
// The artifact toolkit, where the gate is a refusal and there is nothing to redirect.
|
||||||
// v4.4.0 is the first release whose gate carries the localhost test this matches; the
|
// v4.4.0 is the first release whose gate carries the localhost test this matches; the
|
||||||
// releases before it refuse in a shape the runner leaves alone.
|
// releases before it refuse in a shape the runner leaves alone.
|
||||||
{"actions/upload-artifact", "v4.4.0", "dist/upload/index.js"},
|
{"actions/upload-artifact", "v4.4.0", "dist/upload/index.js", true},
|
||||||
{"actions/upload-artifact", "v7.0.1", "dist/upload/index.js"},
|
{"actions/upload-artifact", "v7.0.1", "dist/upload/index.js", true},
|
||||||
{"actions/download-artifact", "v6.0.0", "dist/index.js"},
|
{"actions/download-artifact", "v8.0.1", "dist/index.js", true},
|
||||||
{"oven-sh/setup-bun", "v2.2.0", "dist/setup/index.js"},
|
// Neither toolkit's gate, so these have to come back byte for byte. sccache-action is the
|
||||||
|
// one that exports ACTIONS_CACHE_SERVICE_V2 itself, for the Rust client it installs.
|
||||||
|
{"actions/checkout", "v7.0.1", "dist/index.js", false},
|
||||||
|
{"mozilla-actions/sccache-action", "v0.0.11", "dist/setup/index.js", false},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.repo+"@"+tc.ref, func(t *testing.T) {
|
t.Run(tc.repo+"@"+tc.ref, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -179,7 +229,11 @@ func TestToolkitPatchAcrossActions(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
out, patched := patchedBundle(data)
|
out, patched := patchedBundle(data)
|
||||||
assert.True(t, patched, "the version gate was not patched")
|
require.Equal(t, tc.wantPatched, patched)
|
||||||
|
if !tc.wantPatched {
|
||||||
|
assert.Equal(t, data, out, "an untouched bundle must come back byte for byte")
|
||||||
|
return
|
||||||
|
}
|
||||||
assert.NotContains(t, string(out), ".LOCALHOST", "a copy of the gate was missed")
|
assert.NotContains(t, string(out), ".LOCALHOST", "a copy of the gate was missed")
|
||||||
|
|
||||||
if !strings.Contains(string(data), CacheServiceV2Env) {
|
if !strings.Contains(string(data), CacheServiceV2Env) {
|
||||||
@@ -194,3 +248,121 @@ func TestToolkitPatchAcrossActions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The stock artifact actions refuse on a Gitea host until the gate is opened, and then they talk
|
||||||
|
// to the results service, which is this runner's cache server forwarding the artifact half on to
|
||||||
|
// Gitea. Running the real upload-artifact against a stand-in Gitea covers both halves at once:
|
||||||
|
// the patch, and the forwarding the job's registration set up.
|
||||||
|
func TestUploadArtifactThroughTheResultsService(t *testing.T) {
|
||||||
|
requireHostTools(t, "node")
|
||||||
|
|
||||||
|
var called []string
|
||||||
|
var zipped []byte
|
||||||
|
gitea := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
method := path.Base(r.URL.Path)
|
||||||
|
called = append(called, method)
|
||||||
|
w.Header().Set("x-ms-request-id", "stub")
|
||||||
|
switch method {
|
||||||
|
case "CreateArtifact":
|
||||||
|
_, _ = io.WriteString(w, `{"ok":true,"signed_upload_url":"http://`+r.Host+
|
||||||
|
`/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=x"}`)
|
||||||
|
case "FinalizeArtifact":
|
||||||
|
_, _ = io.WriteString(w, `{"ok":true,"artifact_id":"1"}`)
|
||||||
|
case "ListArtifacts":
|
||||||
|
_, _ = io.WriteString(w, `{"artifacts":[{"workflow_run_backend_id":"11",`+
|
||||||
|
`"workflow_job_run_backend_id":"22","database_id":"1","name":"an-artifact","size":"`+
|
||||||
|
strconv.Itoa(len(zipped))+`"}]}`)
|
||||||
|
case "GetSignedArtifactURL":
|
||||||
|
_, _ = io.WriteString(w, `{"signed_url":"http://`+r.Host+`/download"}`)
|
||||||
|
case "download":
|
||||||
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
|
_, _ = w.Write(zipped)
|
||||||
|
default: // the zip on its way up, in the blocks the Azure protocol puts it in
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
switch r.URL.Query().Get("comp") {
|
||||||
|
case "block":
|
||||||
|
zipped = append(zipped, body...)
|
||||||
|
case "blocklist": // the ordering document, not content
|
||||||
|
default:
|
||||||
|
zipped = body
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer gitea.Close()
|
||||||
|
|
||||||
|
handler, err := artifactcache.StartHandler(filepath.Join(t.TempDir(), "cache"), "127.0.0.1", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = handler.Close() })
|
||||||
|
// The artifact client decodes the runtime token for the run ids it puts in its requests, where
|
||||||
|
// the cache client only presents it, so this one has to be shaped like Gitea's.
|
||||||
|
token := "e30." + base64.RawURLEncoding.EncodeToString([]byte(`{"scp":"Actions.Results:11:22"}`)) + ".sig"
|
||||||
|
defer handler.RegisterJob(token, artifactcache.JobCredential{Repo: "testuser/testrepo", Results: gitea.URL})()
|
||||||
|
|
||||||
|
upload := patchedAction(t, "actions/upload-artifact", "v7.0.1", "dist/upload/index.js")
|
||||||
|
|
||||||
|
env := jobEnv{
|
||||||
|
workspace: tempDirPath(t),
|
||||||
|
runnerTemp: tempDirPath(t),
|
||||||
|
cacheURL: handler.ExternalURL(),
|
||||||
|
resultsURL: handler.ExternalURL(),
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
uploaded := []byte("through the results service")
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(env.workspace, "artifact.txt"), uploaded, 0o600))
|
||||||
|
|
||||||
|
out := runActionEntrypoint(t, upload, env, map[string]string{
|
||||||
|
"name": "an-artifact", "path": "artifact.txt", "if-no-files-found": "error",
|
||||||
|
"retention-days": "0", "compression-level": "6", "overwrite": "false",
|
||||||
|
"include-hidden-files": "false", "archive": "true",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Contains(t, out, "has been successfully uploaded")
|
||||||
|
|
||||||
|
// And back down again: listing and downloading go the same way, and the signed URL the
|
||||||
|
// artifact service hands out is fetched straight from it.
|
||||||
|
download := patchedAction(t, "actions/download-artifact", "v8.0.1", "dist/index.js")
|
||||||
|
env.workspace = tempDirPath(t)
|
||||||
|
out = runActionEntrypoint(t, download, env, map[string]string{
|
||||||
|
"name": "an-artifact", "path": "downloaded", "merge-multiple": "false",
|
||||||
|
"skip-decompress": "false", "include-hidden-files": "false", "github-token": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Contains(t, out, "Artifact download completed")
|
||||||
|
assert.Subset(t, called,
|
||||||
|
[]string{"CreateArtifact", "UploadArtifact", "FinalizeArtifact", "ListArtifacts", "GetSignedArtifactURL"},
|
||||||
|
"the artifact service was not reached through the cache server")
|
||||||
|
got, err := os.ReadFile(filepath.Join(env.workspace, "downloaded", "artifact.txt"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uploaded, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The setup actions carry the same toolkit and reach the same service, from a key of their own
|
||||||
|
// making. setup-node is the cheapest of them to run: given a lockfile and no version to install,
|
||||||
|
// it does the cache lookup and nothing else.
|
||||||
|
func TestSetupActionFindsTheCacheService(t *testing.T) {
|
||||||
|
requireHostTools(t, "node", "npm")
|
||||||
|
|
||||||
|
setup := patchedAction(t, "actions/setup-node", "v7.0.0", "dist/setup/index.js")
|
||||||
|
|
||||||
|
handler, err := artifactcache.StartHandler(filepath.Join(t.TempDir(), "cache"), "127.0.0.1", 0, "", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() { _ = handler.Close() })
|
||||||
|
const token = "setup-runtime-token"
|
||||||
|
defer handler.RegisterJob(token, artifactcache.JobCredential{Repo: "testuser/testrepo"})()
|
||||||
|
|
||||||
|
env := jobEnv{
|
||||||
|
workspace: tempDirPath(t),
|
||||||
|
runnerTemp: tempDirPath(t),
|
||||||
|
cacheURL: handler.ExternalURL(),
|
||||||
|
resultsURL: handler.ExternalURL(),
|
||||||
|
token: token,
|
||||||
|
}
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(env.workspace, "package-lock.json"),
|
||||||
|
[]byte(`{"lockfileVersion":3}`), 0o600))
|
||||||
|
|
||||||
|
out := runActionEntrypoint(t, setup, env, map[string]string{"cache": "npm"})
|
||||||
|
|
||||||
|
require.Contains(t, out, "Cache service version: v2")
|
||||||
|
require.Contains(t, out, "npm cache is not found", "the lookup did not reach the cache server")
|
||||||
|
}
|
||||||
|
|||||||
@@ -283,7 +283,7 @@ func TestPatchBundleAfterTheActionMoved(t *testing.T) {
|
|||||||
// The wiring: a step patches its own bundles only when the runner serves the v2 API, and a step
|
// The wiring: a step patches its own bundles only when the runner serves the v2 API, and a step
|
||||||
// that fails gets them back. The action's path inside its repository is part of where they live.
|
// that fails gets them back. The action's path inside its repository is part of where they live.
|
||||||
func TestStepActionRemoteToolkitPatch(t *testing.T) {
|
func TestStepActionRemoteToolkitPatch(t *testing.T) {
|
||||||
newStep := func(t *testing.T, env map[string]string) (*stepActionRemote, string) {
|
newStep := func(t *testing.T, patch bool) (*stepActionRemote, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
sar := &stepActionRemote{
|
sar := &stepActionRemote{
|
||||||
@@ -291,8 +291,7 @@ func TestStepActionRemoteToolkitPatch(t *testing.T) {
|
|||||||
remoteAction: &remoteAction{Org: "owner", Repo: "repo", Path: "sub", Ref: "v1"},
|
remoteAction: &remoteAction{Org: "owner", Repo: "repo", Path: "sub", Ref: "v1"},
|
||||||
action: &model.Action{Runs: model.ActionRuns{Using: "node20", Main: "index.js"}},
|
action: &model.Action{Runs: model.ActionRuns{Using: "node20", Main: "index.js"}},
|
||||||
RunContext: &RunContext{
|
RunContext: &RunContext{
|
||||||
Env: env,
|
Config: &Config{ActionCacheDir: t.TempDir(), PatchToolkit: patch},
|
||||||
Config: &Config{ActionCacheDir: t.TempDir()},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
script := filepath.Join(sar.actionDir(), "sub", "index.js")
|
script := filepath.Join(sar.actionDir(), "sub", "index.js")
|
||||||
@@ -301,8 +300,8 @@ func TestStepActionRemoteToolkitPatch(t *testing.T) {
|
|||||||
return sar, script
|
return sar, script
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("left alone when the runner does not serve the v2 API", func(t *testing.T) {
|
t.Run("left alone when the runner does not patch", func(t *testing.T) {
|
||||||
sar, script := newStep(t, map[string]string{})
|
sar, script := newStep(t, false)
|
||||||
require.NoError(t, sar.patchActionToolkit(t.Context()))
|
require.NoError(t, sar.patchActionToolkit(t.Context()))
|
||||||
|
|
||||||
body, err := os.ReadFile(script)
|
body, err := os.ReadFile(script)
|
||||||
@@ -311,7 +310,7 @@ func TestStepActionRemoteToolkitPatch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("patched, and put back when the step fails", func(t *testing.T) {
|
t.Run("patched, and put back when the step fails", func(t *testing.T) {
|
||||||
sar, script := newStep(t, map[string]string{CacheServiceV2Env: "true"})
|
sar, script := newStep(t, true)
|
||||||
require.NoError(t, sar.patchActionToolkit(t.Context()))
|
require.NoError(t, sar.patchActionToolkit(t.Context()))
|
||||||
|
|
||||||
body, err := os.ReadFile(script)
|
body, err := os.ReadFile(script)
|
||||||
|
|||||||
24
go.mod
24
go.mod
@@ -11,22 +11,22 @@ require (
|
|||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
github.com/distribution/reference v0.6.0
|
github.com/distribution/reference v0.6.0
|
||||||
github.com/docker/cli v29.6.2+incompatible
|
github.com/docker/cli v29.6.2+incompatible
|
||||||
github.com/docker/go-connections v0.7.0
|
github.com/docker/go-connections v0.8.1
|
||||||
github.com/go-git/go-billy/v5 v5.9.0
|
github.com/go-git/go-billy/v5 v5.9.1
|
||||||
github.com/go-git/go-git/v5 v5.19.1
|
github.com/go-git/go-git/v5 v5.19.1
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/julienschmidt/httprouter v1.3.0
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||||
github.com/mattn/go-isatty v0.0.23
|
github.com/mattn/go-isatty v0.0.24
|
||||||
github.com/moby/go-archive v0.2.0
|
github.com/moby/go-archive v0.2.1
|
||||||
github.com/moby/moby/api v1.55.0
|
github.com/moby/moby/api v1.55.0
|
||||||
github.com/moby/moby/client v0.5.0
|
github.com/moby/moby/client v0.5.1
|
||||||
github.com/moby/patternmatcher v0.6.1
|
github.com/moby/patternmatcher v0.6.1
|
||||||
github.com/opencontainers/image-spec v1.1.1
|
github.com/opencontainers/image-spec v1.1.1
|
||||||
github.com/opencontainers/selinux v1.15.1
|
github.com/opencontainers/selinux v1.15.1
|
||||||
github.com/prometheus/client_golang v1.24.0
|
github.com/prometheus/client_golang v1.24.1
|
||||||
github.com/prometheus/client_model v0.6.2
|
github.com/prometheus/client_model v0.6.2
|
||||||
github.com/rhysd/actionlint v1.7.12
|
github.com/rhysd/actionlint v1.7.12
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
@@ -36,7 +36,7 @@ require (
|
|||||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
||||||
go.etcd.io/bbolt v1.5.0
|
go.etcd.io/bbolt v1.5.0
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||||
golang.org/x/net v0.56.0
|
golang.org/x/net v0.57.0
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.org/x/term v0.45.0
|
golang.org/x/term v0.45.0
|
||||||
golang.org/x/text v0.40.0
|
golang.org/x/text v0.40.0
|
||||||
@@ -72,20 +72,20 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.19.0 // indirect
|
github.com/klauspost/compress v1.19.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
github.com/moby/sys/sequential v0.7.0 // indirect
|
||||||
github.com/moby/sys/user v0.4.0 // indirect
|
github.com/moby/sys/user v0.4.1 // indirect
|
||||||
github.com/moby/sys/userns v0.1.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/common v0.70.0 // indirect
|
github.com/prometheus/common v0.70.1 // indirect
|
||||||
github.com/prometheus/procfs v0.21.1 // indirect
|
github.com/prometheus/procfs v0.21.1 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
@@ -103,7 +103,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
go.opentelemetry.io/otel/trace v1.44.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.53.0 // indirect
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
golang.org/x/sync v0.22.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
48
go.sum
48
go.sum
@@ -49,8 +49,8 @@ github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn
|
|||||||
github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
|
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
|
||||||
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
|
||||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||||
@@ -65,8 +65,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
|||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
|
||||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||||
@@ -100,8 +100,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
|
|||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
|
||||||
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -115,26 +115,26 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
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.1 h1:fAa0wUS/ikZKyx7o/1fhUYmhZ7RgpthdeoDhJvunTLc=
|
||||||
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
github.com/moby/go-archive v0.2.1/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
|
||||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
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/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||||
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
|
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||||
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
|
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||||
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
|
||||||
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
|
||||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
|
||||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
|
||||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
@@ -153,12 +153,12 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
|
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
|
||||||
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
|
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
|
||||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||||
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
|
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
|
||||||
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
|
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
|
||||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||||
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
|
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
|
||||||
@@ -231,13 +231,13 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
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/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
env[actionsRuntimeTokenEnvName] = actionsRuntimeToken
|
env[actionsRuntimeTokenEnvName] = actionsRuntimeToken
|
||||||
os.Setenv(actionsRuntimeTokenEnvName, actionsRuntimeToken)
|
os.Setenv(actionsRuntimeTokenEnvName, actionsRuntimeToken)
|
||||||
}
|
}
|
||||||
handler.RegisterJob(actionsRuntimeToken, "__local/__exec")
|
handler.RegisterJob(actionsRuntimeToken, artifactcache.JobCredential{Repo: "__local/__exec"})
|
||||||
|
|
||||||
// no service aliases: exec builds one config for the whole plan
|
// no service aliases: exec builds one config for the whole plan
|
||||||
run.BypassProxyForDockerHost(os.Getenv("DOCKER_HOST"))
|
run.BypassProxyForDockerHost(os.Getenv("DOCKER_HOST"))
|
||||||
@@ -427,6 +427,7 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command
|
|||||||
config := &runner.Config{
|
config := &runner.Config{
|
||||||
Workdir: execArgs.Workdir(),
|
Workdir: execArgs.Workdir(),
|
||||||
BindWorkdir: false,
|
BindWorkdir: false,
|
||||||
|
PatchToolkit: true, // the cache server started above is what the patch points at
|
||||||
ReuseContainers: false,
|
ReuseContainers: false,
|
||||||
ForcePull: execArgs.forcePull,
|
ForcePull: execArgs.forcePull,
|
||||||
ForceRebuild: execArgs.forceRebuild,
|
ForceRebuild: execArgs.forceRebuild,
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
|||||||
var cacheHandler *artifactcache.Handler
|
var cacheHandler *artifactcache.Handler
|
||||||
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
|
if cfg.Cache.Enabled == nil || *cfg.Cache.Enabled {
|
||||||
if cfg.Cache.ExternalServer != "" {
|
if cfg.Cache.ExternalServer != "" {
|
||||||
envs["ACTIONS_CACHE_URL"] = cfg.Cache.ExternalServer
|
// The v1 client appends its path to this without a separator, so the slash is required.
|
||||||
|
envs["ACTIONS_CACHE_URL"] = strings.TrimRight(cfg.Cache.ExternalServer, "/") + "/"
|
||||||
} else {
|
} else {
|
||||||
warnIgnoredCacheSecret(cfg)
|
warnIgnoredCacheSecret(cfg)
|
||||||
handler, err := artifactcache.StartHandler(
|
handler, err := artifactcache.StartHandler(
|
||||||
@@ -109,12 +110,6 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if envs["ACTIONS_CACHE_URL"] != "" && (cfg.Cache.V2 == nil || *cfg.Cache.V2) {
|
|
||||||
// act patches the GHES check out of an action's bundle when it sees this, so the client
|
|
||||||
// uses the cache service v2 API this server also answers; see act/runner/toolkit_patch.go.
|
|
||||||
envs[runner.CacheServiceV2Env] = "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
// set artifact gitea api
|
// set artifact gitea api
|
||||||
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
|
artifactGiteaAPI := strings.TrimSuffix(cli.Address(), "/") + "/api/actions_pipeline/"
|
||||||
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
|
envs["ACTIONS_RUNTIME_URL"] = artifactGiteaAPI
|
||||||
@@ -440,7 +435,13 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
// lifetime. Only applies to the embedded cache server; when the operator
|
// lifetime. Only applies to the embedded cache server; when the operator
|
||||||
// points the runner at an external cache via cfg.Cache.ExternalServer, it
|
// points the runner at an external cache via cfg.Cache.ExternalServer, it
|
||||||
// is that server's responsibility to authenticate requests.
|
// is that server's responsibility to authenticate requests.
|
||||||
defer r.registerCacheForTask(giteaRuntimeToken, preset.Repository, reporter)()
|
revokeCache, resultsURL := r.registerCacheForTask(giteaRuntimeToken, preset.Repository, reporter)
|
||||||
|
defer revokeCache()
|
||||||
|
// A cache server that agreed to forward the artifact half is the whole results service, so
|
||||||
|
// the job is pointed at it and the v2 variable is finally true.
|
||||||
|
if resultsURL != "" {
|
||||||
|
envs["ACTIONS_RESULTS_URL"], envs[runner.CacheServiceV2Env] = resultsURL, "true"
|
||||||
|
}
|
||||||
|
|
||||||
eventJSON, err := json.Marshal(preset.Event)
|
eventJSON, err := json.Marshal(preset.Event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -481,6 +482,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
AllocatePTY: r.cfg.Runner.AllocatePTY,
|
AllocatePTY: r.cfg.Runner.AllocatePTY,
|
||||||
ActionOfflineMode: r.cfg.Cache.OfflineMode,
|
ActionOfflineMode: r.cfg.Cache.OfflineMode,
|
||||||
ActionCloneDepth: actionCloneDepth,
|
ActionCloneDepth: actionCloneDepth,
|
||||||
|
PatchToolkit: r.patchToolkit(),
|
||||||
|
|
||||||
ReuseContainers: false,
|
ReuseContainers: false,
|
||||||
ForcePull: r.cfg.Container.ForcePull,
|
ForcePull: r.cfg.Container.ForcePull,
|
||||||
@@ -553,6 +555,12 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
return execErr
|
return execErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// patchToolkit reports whether act should edit the toolkit bundled into an action. It follows the
|
||||||
|
// cache URL, because that is what the edits point the client at; see act/runner/toolkit_patch.go.
|
||||||
|
func (r *Runner) patchToolkit() bool {
|
||||||
|
return r.envs["ACTIONS_CACHE_URL"] != "" && (r.cfg.Cache.V2 == nil || *r.cfg.Cache.V2)
|
||||||
|
}
|
||||||
|
|
||||||
// registerCacheForTask tells the cache server to accept requests authenticated
|
// registerCacheForTask tells the cache server to accept requests authenticated
|
||||||
// with the given runtime token for the duration of this task. Returns a
|
// with the given runtime token for the duration of this task. Returns a
|
||||||
// function the caller must invoke (typically via defer) to revoke the
|
// function the caller must invoke (typically via defer) to revoke the
|
||||||
@@ -565,18 +573,25 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
// repo scoping over the network.
|
// repo scoping over the network.
|
||||||
//
|
//
|
||||||
// Safe with an empty token (older Gitea did not issue one).
|
// Safe with an empty token (older Gitea did not issue one).
|
||||||
func (r *Runner) registerCacheForTask(token, repo string, reporter *report.Reporter) func() {
|
// It also returns the URL to advertise as ACTIONS_RESULTS_URL, which is the cache server itself
|
||||||
|
// when it agreed to forward this instance's artifact service, and "" when it did not.
|
||||||
|
func (r *Runner) registerCacheForTask(token, repo string, reporter *report.Reporter) (func(), string) {
|
||||||
if token == "" {
|
if token == "" {
|
||||||
return func() {}
|
return func() {}, ""
|
||||||
|
}
|
||||||
|
cred := artifactcache.JobCredential{
|
||||||
|
Repo: repo,
|
||||||
|
Results: r.envs["ACTIONS_RESULTS_URL"], // the instance as the job would reach it
|
||||||
|
InsecureTLS: r.cfg.Runner.Insecure,
|
||||||
}
|
}
|
||||||
if r.cacheHandler != nil {
|
if r.cacheHandler != nil {
|
||||||
return r.cacheHandler.RegisterJob(token, repo)
|
return r.cacheHandler.RegisterJob(token, cred), r.cacheHandler.ResultsURL(cred)
|
||||||
}
|
}
|
||||||
if r.cfg.Cache.ExternalServer != "" && r.cfg.Cache.ExternalSecret != "" {
|
if r.cfg.Cache.ExternalServer != "" && r.cfg.Cache.ExternalSecret != "" {
|
||||||
return r.registerExternalCacheJob(token, repo, reporter)
|
return r.registerExternalCacheJob(token, cred, reporter)
|
||||||
}
|
}
|
||||||
// No cache server to register against: caching is disabled, or the built-in server failed to start.
|
// No cache server to register against: caching is disabled, or the built-in server failed to start.
|
||||||
return func() {}
|
return func() {}, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerExternalCacheJob POSTs to the remote cache-server's control-plane.
|
// registerExternalCacheJob POSTs to the remote cache-server's control-plane.
|
||||||
@@ -584,47 +599,54 @@ func (r *Runner) registerCacheForTask(token, repo string, reporter *report.Repor
|
|||||||
// 401 the job's requests — better than failing the whole task for a cache
|
// 401 the job's requests — better than failing the whole task for a cache
|
||||||
// outage. The warning is mirrored to the job log so users can see why their
|
// outage. The warning is mirrored to the job log so users can see why their
|
||||||
// cache calls 401, instead of having to read the runner daemon's stderr.
|
// cache calls 401, instead of having to read the runner daemon's stderr.
|
||||||
func (r *Runner) registerExternalCacheJob(token, repo string, reporter *report.Reporter) func() {
|
func (r *Runner) registerExternalCacheJob(token string, cred artifactcache.JobCredential, reporter *report.Reporter) (func(), string) {
|
||||||
base := strings.TrimRight(r.cfg.Cache.ExternalServer, "/")
|
base := strings.TrimRight(r.cfg.Cache.ExternalServer, "/")
|
||||||
if err := postInternalCache(base+"/_internal/register", r.cfg.Cache.ExternalSecret,
|
resultsURL := ""
|
||||||
map[string]string{"token": token, "repo": repo}); err != nil {
|
if body, err := postInternalCache(base+"/_internal/register", r.cfg.Cache.ExternalSecret, map[string]any{
|
||||||
|
"token": token, "repo": cred.Repo, "results": cred.Results, "insecure_tls": cred.InsecureTLS,
|
||||||
|
}); err != nil {
|
||||||
log.Warnf("cache external_server register failed (%s): %v", base, err)
|
log.Warnf("cache external_server register failed (%s): %v", base, err)
|
||||||
if reporter != nil {
|
if reporter != nil {
|
||||||
reporter.Logf("::warning::cache external_server register failed (%s): %v — cache requests from this job will be unauthenticated and likely return 401", base, err)
|
reporter.Logf("::warning::cache external_server register failed (%s): %v — cache requests from this job will be unauthenticated and likely return 401", base, err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
resultsURL, _ = body["results_url"].(string) // absent from a server too old to forward
|
||||||
}
|
}
|
||||||
return func() {
|
return func() {
|
||||||
if err := postInternalCache(base+"/_internal/revoke", r.cfg.Cache.ExternalSecret,
|
if _, err := postInternalCache(base+"/_internal/revoke", r.cfg.Cache.ExternalSecret,
|
||||||
map[string]string{"token": token}); err != nil {
|
map[string]any{"token": token}); err != nil {
|
||||||
log.Warnf("cache external_server revoke failed (%s): %v", base, err)
|
log.Warnf("cache external_server revoke failed (%s): %v", base, err)
|
||||||
if reporter != nil {
|
if reporter != nil {
|
||||||
reporter.Logf("::warning::cache external_server revoke failed (%s): %v", base, err)
|
reporter.Logf("::warning::cache external_server revoke failed (%s): %v", base, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, resultsURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func postInternalCache(url, secret string, body map[string]string) error {
|
func postInternalCache(url, secret string, body map[string]any) (map[string]any, error) {
|
||||||
buf, err := json.Marshal(body)
|
buf, err := json.Marshal(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(buf))
|
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(buf))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+secret)
|
req.Header.Set("Authorization", "Bearer "+secret)
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
client := &http.Client{Timeout: 5 * time.Second}
|
client := &http.Client{Timeout: 5 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode/100 != 2 {
|
if resp.StatusCode/100 != 2 {
|
||||||
return fmt.Errorf("status %d", resp.StatusCode)
|
return nil, fmt.Errorf("status %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
return nil
|
answer := map[string]any{}
|
||||||
|
// A server too old to answer with a body is not an error, it simply tells us nothing.
|
||||||
|
_ = json.NewDecoder(resp.Body).Decode(&answer)
|
||||||
|
return answer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) RunningCount() int64 {
|
func (r *Runner) RunningCount() int64 {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -29,7 +31,7 @@ func TestRunner_registerCacheForTask(t *testing.T) {
|
|||||||
|
|
||||||
r := &Runner{cfg: emptyCfg(), cacheHandler: handler}
|
r := &Runner{cfg: emptyCfg(), cacheHandler: handler}
|
||||||
token := "run-token-123"
|
token := "run-token-123"
|
||||||
unregister := r.registerCacheForTask(token, "owner/repo", nil)
|
unregister, _ := r.registerCacheForTask(token, "owner/repo", nil)
|
||||||
|
|
||||||
base := handler.ExternalURL() + "/_apis/artifactcache"
|
base := handler.ExternalURL() + "/_apis/artifactcache"
|
||||||
probe := func() int {
|
probe := func() int {
|
||||||
@@ -53,7 +55,7 @@ func TestRunner_registerCacheForTask(t *testing.T) {
|
|||||||
func TestRunner_registerCacheForTask_NoOps(t *testing.T) {
|
func TestRunner_registerCacheForTask_NoOps(t *testing.T) {
|
||||||
t.Run("nil cacheHandler", func(t *testing.T) {
|
t.Run("nil cacheHandler", func(t *testing.T) {
|
||||||
r := &Runner{cfg: emptyCfg()}
|
r := &Runner{cfg: emptyCfg()}
|
||||||
unregister := r.registerCacheForTask("tok", "owner/repo", nil)
|
unregister, _ := r.registerCacheForTask("tok", "owner/repo", nil)
|
||||||
require.NotNil(t, unregister)
|
require.NotNil(t, unregister)
|
||||||
unregister()
|
unregister()
|
||||||
})
|
})
|
||||||
@@ -65,7 +67,7 @@ func TestRunner_registerCacheForTask_NoOps(t *testing.T) {
|
|||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
|
|
||||||
r := &Runner{cfg: emptyCfg(), cacheHandler: handler}
|
r := &Runner{cfg: emptyCfg(), cacheHandler: handler}
|
||||||
unregister := r.registerCacheForTask("", "owner/repo", nil)
|
unregister, _ := r.registerCacheForTask("", "owner/repo", nil)
|
||||||
require.NotNil(t, unregister)
|
require.NotNil(t, unregister)
|
||||||
unregister()
|
unregister()
|
||||||
})
|
})
|
||||||
@@ -81,7 +83,7 @@ func TestRunner_CacheFullFlow_MatchesToolkit(t *testing.T) {
|
|||||||
|
|
||||||
r := &Runner{cfg: emptyCfg(), cacheHandler: handler}
|
r := &Runner{cfg: emptyCfg(), cacheHandler: handler}
|
||||||
token := "full-flow-token"
|
token := "full-flow-token"
|
||||||
unregister := r.registerCacheForTask(token, "owner/repo", nil)
|
unregister, _ := r.registerCacheForTask(token, "owner/repo", nil)
|
||||||
defer unregister()
|
defer unregister()
|
||||||
|
|
||||||
base := handler.ExternalURL() + "/_apis/artifactcache"
|
base := handler.ExternalURL() + "/_apis/artifactcache"
|
||||||
@@ -154,18 +156,27 @@ func decodeJSON(resp *http.Response, v any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// End-to-end against a remote cache-server: token unknown → 401, register →
|
// End-to-end against a remote cache-server: token unknown → 401, register →
|
||||||
// reserve/upload/commit/find/download all OK, revoke → 401 again.
|
// reserve/upload/commit/find/download all OK, revoke → 401 again. Registering also names the
|
||||||
|
// instance, and the server answering with its own address is what makes a shared cache server the
|
||||||
|
// whole results service, as the built-in one is.
|
||||||
func TestRunner_ExternalCacheServer_RegisterRevoke(t *testing.T) {
|
func TestRunner_ExternalCacheServer_RegisterRevoke(t *testing.T) {
|
||||||
dir := filepath.Join(t.TempDir(), "remote-cache")
|
dir := filepath.Join(t.TempDir(), "remote-cache")
|
||||||
const secret = "shared-secret-for-tests"
|
const secret = "shared-secret-for-tests"
|
||||||
remote, err := artifactcache.StartHandler(dir, "127.0.0.1", 0, secret, nil)
|
remote, err := artifactcache.StartHandler(dir, "127.0.0.1", 0, secret, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer remote.Close()
|
defer remote.Close()
|
||||||
|
gitea := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
_, _ = io.WriteString(w, `{"ok":true}`)
|
||||||
|
}))
|
||||||
|
defer gitea.Close()
|
||||||
|
|
||||||
r := &Runner{cfg: &config.Config{Cache: config.Cache{
|
r := &Runner{
|
||||||
|
cfg: &config.Config{Cache: config.Cache{
|
||||||
ExternalServer: remote.ExternalURL(),
|
ExternalServer: remote.ExternalURL(),
|
||||||
ExternalSecret: secret,
|
ExternalSecret: secret,
|
||||||
}}}
|
}},
|
||||||
|
envs: map[string]string{"ACTIONS_RESULTS_URL": gitea.URL},
|
||||||
|
}
|
||||||
|
|
||||||
token := "external-task-token"
|
token := "external-task-token"
|
||||||
repo := "owner/repoX"
|
repo := "owner/repoX"
|
||||||
@@ -182,10 +193,21 @@ func TestRunner_ExternalCacheServer_RegisterRevoke(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusUnauthorized, probe(),
|
require.Equal(t, http.StatusUnauthorized, probe(),
|
||||||
"token must be unknown to the remote server before registration")
|
"token must be unknown to the remote server before registration")
|
||||||
|
|
||||||
unregister := r.registerCacheForTask(token, repo, nil)
|
unregister, resultsURL := r.registerCacheForTask(token, repo, nil)
|
||||||
require.NotEqual(t, http.StatusUnauthorized, probe(),
|
require.NotEqual(t, http.StatusUnauthorized, probe(),
|
||||||
"token must be accepted after registerCacheForTask")
|
"token must be accepted after registerCacheForTask")
|
||||||
|
|
||||||
|
// The server took the results service over, so the artifact half reaches Gitea through it.
|
||||||
|
require.Equal(t, remote.ExternalURL(), resultsURL)
|
||||||
|
artifact, err := http.NewRequestWithContext(t.Context(), http.MethodPost,
|
||||||
|
resultsURL+"/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
artifact.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
forwarded, err := http.DefaultClient.Do(artifact)
|
||||||
|
require.NoError(t, err)
|
||||||
|
forwarded.Body.Close()
|
||||||
|
require.Equal(t, http.StatusOK, forwarded.StatusCode)
|
||||||
|
|
||||||
// Full reserve→upload→commit→find→download cycle, identical to what
|
// Full reserve→upload→commit→find→download cycle, identical to what
|
||||||
// @actions/cache does, against the remote (external) server.
|
// @actions/cache does, against the remote (external) server.
|
||||||
body := []byte("payload-from-task")
|
body := []byte("payload-from-task")
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package run
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
@@ -124,21 +126,57 @@ func taskWithDefaultActionsURL(url string) *runnerv1.Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The cache service v2 API is announced to jobs unless it is turned off. Announcing it is what
|
// The results service is decided per task, because that is where the job's token and its
|
||||||
// makes act patch the GHES check out of an action's bundle, so the client can reach it.
|
// instance are both known. NewRunner only leaves the address Gitea serves.
|
||||||
func TestNewRunnerCacheServiceV2(t *testing.T) {
|
func TestNewRunnerCacheServiceV2(t *testing.T) {
|
||||||
announced := func(v2 *bool) string {
|
|
||||||
cfg := &config.Config{}
|
cfg := &config.Config{}
|
||||||
cfg.Cache.V2, cfg.Cache.Dir, cfg.Cache.Host = v2, t.TempDir(), "127.0.0.1"
|
cfg.Cache.Dir, cfg.Cache.Host = t.TempDir(), "127.0.0.1"
|
||||||
cli := clientmocks.NewClient(t)
|
cli := clientmocks.NewClient(t)
|
||||||
cli.On("Address").Return("https://gitea.example/").Maybe()
|
cli.On("Address").Return("https://gitea.example/").Maybe()
|
||||||
|
|
||||||
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
|
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
|
||||||
t.Cleanup(func() { _ = r.Close() })
|
t.Cleanup(func() { _ = r.Close() })
|
||||||
return r.envs[runner.CacheServiceV2Env]
|
const token = "task-token"
|
||||||
|
|
||||||
|
assert.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
|
||||||
|
assert.Empty(t, r.envs[runner.CacheServiceV2Env], "a promise the runner has not made yet")
|
||||||
|
assert.True(t, r.patchToolkit())
|
||||||
|
|
||||||
|
// The registration is what makes it true: the cache server takes the results service over,
|
||||||
|
// having been told which instance to forward the artifact half to.
|
||||||
|
revoke, resultsURL := r.registerCacheForTask(token, "owner/repo", nil)
|
||||||
|
defer revoke()
|
||||||
|
require.Equal(t, r.cacheHandler.ExternalURL(), resultsURL)
|
||||||
|
|
||||||
|
// And what is advertised has to answer the cache service. A client without the GHES escape
|
||||||
|
// hatch, docker buildx among them, posts its cache calls at exactly this URL and nowhere else,
|
||||||
|
// so a 404 here is the failure this whole change is about.
|
||||||
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost,
|
||||||
|
resultsURL+"/twirp/github.actions.results.api.v1.CacheService/GetCacheEntryDownloadURL",
|
||||||
|
strings.NewReader(`{"key":"k","version":"v"}`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "the advertised results service serves no cache service")
|
||||||
}
|
}
|
||||||
|
|
||||||
off := false
|
// The v1 cache client appends its path to ACTIONS_CACHE_URL without a separator, so a configured
|
||||||
assert.Equal(t, "true", announced(nil))
|
// server that is missing the slash would send it to a URL whose port swallows the path.
|
||||||
assert.Empty(t, announced(&off))
|
func TestNewRunnerNormalizesTheExternalCacheServer(t *testing.T) {
|
||||||
|
cfg := &config.Config{}
|
||||||
|
cfg.Cache.ExternalServer = "http://cache.local:8088//"
|
||||||
|
cli := clientmocks.NewClient(t)
|
||||||
|
cli.On("Address").Return("https://gitea.example/").Maybe()
|
||||||
|
|
||||||
|
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
|
||||||
|
|
||||||
|
assert.Equal(t, "http://cache.local:8088/", r.envs["ACTIONS_CACHE_URL"])
|
||||||
|
// Nothing to front the results service with, so the variable stays unset, but the bundles are
|
||||||
|
// still patched: artifacts v4 need that, and the patch keeps the cache client on the cache URL.
|
||||||
|
assert.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
|
||||||
|
assert.Empty(t, r.envs[runner.CacheServiceV2Env])
|
||||||
|
assert.True(t, r.patchToolkit())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ cache:
|
|||||||
# Ignored when external_server is set.
|
# Ignored when external_server is set.
|
||||||
port: 0
|
port: 0
|
||||||
# URL of a shared `gitea-runner cache-server` to use instead of starting a local one.
|
# URL of a shared `gitea-runner cache-server` to use instead of starting a local one.
|
||||||
# Set on every runner that should share a cache pool. Must end with "/".
|
# Set on every runner that should share a cache pool. A trailing slash is optional.
|
||||||
# Example: "http://cache-host:8088/"
|
# Example: "http://cache-host:8088/"
|
||||||
# Requires external_secret (below) to match the value on the cache-server.
|
# Requires external_secret (below) to match the value on the cache-server.
|
||||||
external_server: ""
|
external_server: ""
|
||||||
|
|||||||
Reference in New Issue
Block a user