mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-06-10 20:04:24 +02:00
Compare commits
36 Commits
dff63b3ecc
...
v1.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47ee45412a | ||
|
|
38b69bb214 | ||
|
|
1c62c0635f | ||
|
|
0e0c54b272 | ||
|
|
d6fbe75721 | ||
|
|
b30204aa94 | ||
|
|
7b5ebe9618 | ||
|
|
4317662a38 | ||
|
|
2208e7ec63 | ||
|
|
fab9714f9a | ||
|
|
10475db58a | ||
|
|
9e738c203c | ||
|
|
6023928876 | ||
|
|
014ce438c1 | ||
|
|
cf7e29c10d | ||
|
|
8a99506fed | ||
|
|
5873b8b054 | ||
|
|
5464d33eef | ||
|
|
3c5f03ff8f | ||
|
|
880e9755d9 | ||
|
|
8d7cf48a6f | ||
|
|
f23605c614 | ||
|
|
00b7fec80f | ||
|
|
dda5841af8 | ||
|
|
32bed52686 | ||
|
|
a7e972d8de | ||
|
|
763b38ece3 | ||
|
|
a1f13cb970 | ||
|
|
1e3ab0c40a | ||
|
|
295eecb9af | ||
|
|
ef6ca957b5 | ||
|
|
8088df52b9 | ||
|
|
3ea7d39690 | ||
|
|
861d351845 | ||
|
|
cce8543d06 | ||
|
|
75643645f0 |
27
.gitea/workflows/pull-pr-title.yml
Normal file
27
.gitea/workflows/pull-pr-title.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: pr-title
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
- ready_for_review
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-pr-title:
|
||||||
|
if: github.event.pull_request.draft == false
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
- run: make lint-pr-title
|
||||||
|
env:
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: goreleaser
|
- name: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
args: release --nightly
|
args: release --nightly
|
||||||
@@ -57,13 +57,13 @@ jobs:
|
|||||||
fetch-depth: 0 # all history for all branches and tags
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker BuildX
|
- name: Set up Docker BuildX
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
@@ -71,8 +71,13 @@ jobs:
|
|||||||
- name: Echo the tag
|
- name: Echo the tag
|
||||||
run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}"
|
run: echo "${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}"
|
||||||
|
|
||||||
|
- name: Get Meta
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
echo REPO_VERSION=$(git describe --tags --always | sed 's/-/+/' | sed 's/^v//') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -83,3 +88,5 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
${{ env.DOCKER_ORG }}/runner:nightly${{ matrix.variant.tag_suffix }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.meta.outputs.REPO_VERSION }}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
passphrase: ${{ secrets.PASSPHRASE }}
|
passphrase: ${{ secrets.PASSPHRASE }}
|
||||||
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
fingerprint: CC64B1DB67ABBEECAB24B6455FC346329753F4B0
|
||||||
- name: goreleaser
|
- name: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v7
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser-pro
|
distribution: goreleaser-pro
|
||||||
args: release
|
args: release
|
||||||
@@ -60,20 +60,20 @@ jobs:
|
|||||||
fetch-depth: 0 # all history for all branches and tags
|
fetch-depth: 0 # all history for all branches and tags
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker BuildX
|
- name: Set up Docker BuildX
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: "Docker meta"
|
- name: "Docker meta"
|
||||||
id: docker_meta
|
id: docker_meta
|
||||||
uses: https://github.com/docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.DOCKER_ORG }}/runner
|
${{ env.DOCKER_ORG }}/runner
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
suffix=${{ matrix.variant.tag_suffix }},onlatest=true
|
suffix=${{ matrix.variant.tag_suffix }},onlatest=true
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -96,3 +96,5 @@ jobs:
|
|||||||
linux/arm64
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.docker_meta.outputs.version }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
/gitea-runner
|
/gitea-runner
|
||||||
.env
|
.env
|
||||||
|
!/act/runner/testdata/secrets/.env
|
||||||
.runner
|
.runner
|
||||||
coverage.txt
|
coverage.txt
|
||||||
/config.yaml
|
/config.yaml
|
||||||
@@ -10,4 +11,4 @@ coverage.txt
|
|||||||
.vscode
|
.vscode
|
||||||
__debug_bin
|
__debug_bin
|
||||||
# gorelease binary folder
|
# gorelease binary folder
|
||||||
dist
|
/dist
|
||||||
|
|||||||
24
Dockerfile
24
Dockerfile
@@ -1,7 +1,7 @@
|
|||||||
### BUILDER STAGE
|
### BUILDER STAGE
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM golang:1.26-alpine AS builder
|
FROM golang:1.26-alpine3.23 AS builder
|
||||||
|
|
||||||
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
# Do not remove `git` here, it is required for getting runner version when executing `make build`
|
||||||
RUN apk add --no-cache make git
|
RUN apk add --no-cache make git
|
||||||
@@ -17,7 +17,12 @@ RUN make clean && make build
|
|||||||
### DIND VARIANT
|
### DIND VARIANT
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM docker:29-dind AS dind
|
FROM docker:29.5.2-dind AS dind
|
||||||
|
|
||||||
|
ARG VERSION=dev
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
|
||||||
|
LABEL org.opencontainers.image.version="${VERSION}"
|
||||||
|
|
||||||
RUN apk add --no-cache s6 bash git tzdata
|
RUN apk add --no-cache s6 bash git tzdata
|
||||||
|
|
||||||
@@ -32,7 +37,12 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
|||||||
### DIND-ROOTLESS VARIANT
|
### DIND-ROOTLESS VARIANT
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM docker:29-dind-rootless AS dind-rootless
|
FROM docker:29.5.2-dind-rootless AS dind-rootless
|
||||||
|
|
||||||
|
ARG VERSION=dev
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
|
||||||
|
LABEL org.opencontainers.image.version="${VERSION}"
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
RUN apk add --no-cache s6 bash git tzdata
|
RUN apk add --no-cache s6 bash git tzdata
|
||||||
@@ -53,7 +63,13 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
|||||||
### BASIC VARIANT
|
### BASIC VARIANT
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
FROM alpine AS basic
|
FROM alpine:3.23 AS basic
|
||||||
|
|
||||||
|
ARG VERSION=dev
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source="https://gitea.com/gitea/runner"
|
||||||
|
LABEL org.opencontainers.image.version="${VERSION}"
|
||||||
|
|
||||||
RUN apk add --no-cache tini bash git tzdata
|
RUN apk add --no-cache tini bash git tzdata
|
||||||
|
|
||||||
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
COPY --from=builder /opt/src/runner/gitea-runner /usr/local/bin/gitea-runner
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -18,8 +18,8 @@ DOCKER_TAG ?= nightly
|
|||||||
DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)
|
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.11.4
|
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0
|
||||||
|
|
||||||
STATIC ?=
|
STATIC ?=
|
||||||
EXTLDFLAGS ?=
|
EXTLDFLAGS ?=
|
||||||
@@ -118,6 +118,10 @@ lint-go: ## lint go files
|
|||||||
lint-go-fix: ## lint go files and fix issues
|
lint-go-fix: ## lint go files and fix issues
|
||||||
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
$(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
|
||||||
|
|
||||||
|
.PHONY: lint-pr-title
|
||||||
|
lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...)
|
||||||
|
@node ./tools/lint-pr-title.ts
|
||||||
|
|
||||||
.PHONY: security-check
|
.PHONY: security-check
|
||||||
security-check: deps-tools
|
security-check: deps-tools
|
||||||
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
GOEXPERIMENT= $(GO) run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||||
|
|||||||
@@ -325,10 +325,6 @@ func (h *Handler) openDB() (*bolthold.Store, error) {
|
|||||||
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
cred := credFromContext(r.Context())
|
cred := credFromContext(r.Context())
|
||||||
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
keys := strings.Split(r.URL.Query().Get("keys"), ",")
|
||||||
// cache keys are case insensitive
|
|
||||||
for i, key := range keys {
|
|
||||||
keys[i] = strings.ToLower(key)
|
|
||||||
}
|
|
||||||
version := r.URL.Query().Get("version")
|
version := r.URL.Query().Get("version")
|
||||||
|
|
||||||
db, err := h.openDB()
|
db, err := h.openDB()
|
||||||
@@ -371,8 +367,6 @@ func (h *Handler) reserve(w http.ResponseWriter, r *http.Request, _ httprouter.P
|
|||||||
h.responseJSON(w, r, 400, err)
|
h.responseJSON(w, r, 400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// cache keys are case insensitive
|
|
||||||
api.Key = strings.ToLower(api.Key)
|
|
||||||
|
|
||||||
cache := api.ToCache()
|
cache := api.ToCache()
|
||||||
cache.Repo = cred.Repo
|
cache.Repo = cred.Repo
|
||||||
@@ -437,6 +431,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request, params httprout
|
|||||||
}
|
}
|
||||||
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
if err := h.storage.Write(cache.ID, start, r.Body); err != nil {
|
||||||
h.responseJSON(w, r, 500, err)
|
h.responseJSON(w, r, 500, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
h.useCache(id)
|
h.useCache(id)
|
||||||
h.responseJSON(w, r, 200)
|
h.responseJSON(w, r, 200)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -338,6 +339,54 @@ func TestHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("upload write failure returns only error", func(t *testing.T) {
|
||||||
|
key := strings.ToLower(t.Name())
|
||||||
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
|
var id uint64
|
||||||
|
{
|
||||||
|
body, err := json.Marshal(&Request{
|
||||||
|
Key: key,
|
||||||
|
Version: version,
|
||||||
|
Size: 100,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := testClient.Post(base+"/caches", "application/json", bytes.NewReader(body))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
|
got := struct {
|
||||||
|
CacheID uint64 `json:"cacheId"`
|
||||||
|
}{}
|
||||||
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||||
|
id = got.CacheID
|
||||||
|
}
|
||||||
|
|
||||||
|
storageFile := filepath.Join(dir, "not-a-directory")
|
||||||
|
require.NoError(t, os.WriteFile(storageFile, []byte("blocked"), 0o600))
|
||||||
|
originalStorage := handler.storage
|
||||||
|
handler.storage = &Storage{rootDir: storageFile}
|
||||||
|
defer func() {
|
||||||
|
handler.storage = originalStorage
|
||||||
|
}()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPatch,
|
||||||
|
fmt.Sprintf("%s/caches/%d", base, id), bytes.NewReader(make([]byte, 100)))
|
||||||
|
require.NoError(t, err)
|
||||||
|
req.Header.Set("Content-Type", "application/octet-stream")
|
||||||
|
req.Header.Set("Content-Range", "bytes 0-99/*")
|
||||||
|
resp, err := testClient.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
require.Equal(t, 500, resp.StatusCode)
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
var got map[string]string
|
||||||
|
require.NoError(t, json.Unmarshal(body, &got))
|
||||||
|
assert.NotEmpty(t, got["error"])
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("commit early", func(t *testing.T) {
|
t.Run("commit early", func(t *testing.T) {
|
||||||
key := strings.ToLower(t.Name())
|
key := strings.ToLower(t.Name())
|
||||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
@@ -459,17 +508,20 @@ func TestHandler(t *testing.T) {
|
|||||||
assert.Equal(t, contents[except], content)
|
assert.Equal(t, contents[except], content)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("case insensitive", func(t *testing.T) {
|
t.Run("case preserved", func(t *testing.T) {
|
||||||
|
// Some actions (e.g. actions/setup-go, actions/setup-node) build cache keys that contain mixed-case fragments such as RUNNER_OS=Linux,
|
||||||
|
// then compare the cacheKey returned by the cache server to their original key with case-sensitive equality to decide whether the
|
||||||
|
// cache was a complete hit. The server must therefore preserve the original key case.
|
||||||
|
|
||||||
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
version := "c19da02a2bd7e77277f1ac29ab45c09b7d46a4ee758284e26bb3045ad11d9d20"
|
||||||
key := strings.ToLower(t.Name())
|
key := strings.ToLower(t.Name()) + "_ABC"
|
||||||
content := make([]byte, 100)
|
content := make([]byte, 100)
|
||||||
_, err := rand.Read(content)
|
_, err := rand.Read(content)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
uploadCacheNormally(t, base, key+"_ABC", version, content)
|
uploadCacheNormally(t, base, key, version, content)
|
||||||
|
|
||||||
{
|
{
|
||||||
reqKey := key + "_aBc"
|
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, key, version))
|
||||||
resp, err := testClient.Get(fmt.Sprintf("%s/cache?keys=%s&version=%s", base, reqKey, version))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
@@ -480,7 +532,8 @@ func TestHandler(t *testing.T) {
|
|||||||
}{}
|
}{}
|
||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||||
assert.Equal(t, "hit", got.Result)
|
assert.Equal(t, "hit", got.Result)
|
||||||
assert.Equal(t, key+"_abc", got.CacheKey)
|
assert.Equal(t, key, got.CacheKey)
|
||||||
|
assert.NotEqual(t, strings.ToLower(key), got.CacheKey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -643,7 +696,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte
|
|||||||
}{}
|
}{}
|
||||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
require.NoError(t, json.NewDecoder(resp.Body).Decode(&got))
|
||||||
assert.Equal(t, "hit", got.Result)
|
assert.Equal(t, "hit", got.Result)
|
||||||
assert.Equal(t, strings.ToLower(key), got.CacheKey)
|
assert.Equal(t, key, got.CacheKey)
|
||||||
archiveLocation = got.ArchiveLocation
|
archiveLocation = got.ArchiveLocation
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
package common
|
package common
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
// CartesianProduct takes map of lists and returns list of unique tuples
|
// CartesianProduct takes map of lists and returns list of unique tuples
|
||||||
func CartesianProduct(mapOfLists map[string][]any) []map[string]any {
|
func CartesianProduct(mapOfLists map[string][]any) []map[string]any {
|
||||||
listNames := make([]string, 0)
|
listNames := make([]string, 0)
|
||||||
@@ -46,7 +48,7 @@ func cartN(a ...[]any) [][]any {
|
|||||||
for j, n := range n {
|
for j, n := range n {
|
||||||
pi[j] = a[j][n]
|
pi[j] = a[j][n]
|
||||||
}
|
}
|
||||||
for j := len(n) - 1; j >= 0; j-- {
|
for j := range slices.Backward(n) {
|
||||||
n[j]++
|
n[j]++
|
||||||
if n[j] < len(a[j]) {
|
if n[j] < len(a[j]) {
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Style is a specific style
|
|
||||||
type Style int
|
|
||||||
|
|
||||||
// Styles
|
|
||||||
const (
|
|
||||||
StyleDoubleLine = iota
|
|
||||||
StyleSingleLine
|
|
||||||
StyleDashedLine
|
|
||||||
StyleNoLine
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewPen creates a new pen
|
|
||||||
func NewPen(style Style, color int) *Pen {
|
|
||||||
bgcolor := 49
|
|
||||||
if os.Getenv("CLICOLOR") == "0" {
|
|
||||||
color = 0
|
|
||||||
bgcolor = 0
|
|
||||||
}
|
|
||||||
return &Pen{
|
|
||||||
style: style,
|
|
||||||
color: color,
|
|
||||||
bgcolor: bgcolor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type styleDef struct {
|
|
||||||
cornerTL string
|
|
||||||
cornerTR string
|
|
||||||
cornerBL string
|
|
||||||
cornerBR string
|
|
||||||
lineH string
|
|
||||||
lineV string
|
|
||||||
}
|
|
||||||
|
|
||||||
var styleDefs = []styleDef{
|
|
||||||
{"\u2554", "\u2557", "\u255a", "\u255d", "\u2550", "\u2551"},
|
|
||||||
{"\u256d", "\u256e", "\u2570", "\u256f", "\u2500", "\u2502"},
|
|
||||||
{"\u250c", "\u2510", "\u2514", "\u2518", "\u254c", "\u254e"},
|
|
||||||
{" ", " ", " ", " ", " ", " "},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pen struct
|
|
||||||
type Pen struct {
|
|
||||||
style Style
|
|
||||||
color int
|
|
||||||
bgcolor int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drawing struct
|
|
||||||
type Drawing struct {
|
|
||||||
buf *strings.Builder
|
|
||||||
width int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pen) drawTopBars(buf io.Writer, labels ...string) {
|
|
||||||
style := styleDefs[p.style]
|
|
||||||
for _, label := range labels {
|
|
||||||
bar := strings.Repeat(style.lineH, len(label)+2)
|
|
||||||
fmt.Fprintf(buf, " ")
|
|
||||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
|
||||||
fmt.Fprintf(buf, "%s%s%s", style.cornerTL, bar, style.cornerTR)
|
|
||||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(buf, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pen) drawBottomBars(buf io.Writer, labels ...string) {
|
|
||||||
style := styleDefs[p.style]
|
|
||||||
for _, label := range labels {
|
|
||||||
bar := strings.Repeat(style.lineH, len(label)+2)
|
|
||||||
fmt.Fprintf(buf, " ")
|
|
||||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
|
||||||
fmt.Fprintf(buf, "%s%s%s", style.cornerBL, bar, style.cornerBR)
|
|
||||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(buf, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pen) drawLabels(buf io.Writer, labels ...string) {
|
|
||||||
style := styleDefs[p.style]
|
|
||||||
for _, label := range labels {
|
|
||||||
fmt.Fprintf(buf, " ")
|
|
||||||
fmt.Fprintf(buf, "\x1b[%d;%dm", p.color, p.bgcolor)
|
|
||||||
fmt.Fprintf(buf, "%s %s %s", style.lineV, label, style.lineV)
|
|
||||||
fmt.Fprintf(buf, "\x1b[%dm", 0)
|
|
||||||
}
|
|
||||||
fmt.Fprintf(buf, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawArrow between boxes
|
|
||||||
func (p *Pen) DrawArrow() *Drawing {
|
|
||||||
drawing := &Drawing{
|
|
||||||
buf: new(strings.Builder),
|
|
||||||
width: 1,
|
|
||||||
}
|
|
||||||
fmt.Fprintf(drawing.buf, "\x1b[%dm", p.color)
|
|
||||||
fmt.Fprintf(drawing.buf, "\u2b07")
|
|
||||||
fmt.Fprintf(drawing.buf, "\x1b[%dm", 0)
|
|
||||||
return drawing
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawBoxes to draw boxes
|
|
||||||
func (p *Pen) DrawBoxes(labels ...string) *Drawing {
|
|
||||||
width := 0
|
|
||||||
for _, l := range labels {
|
|
||||||
width += len(l) + 2 + 2 + 1
|
|
||||||
}
|
|
||||||
drawing := &Drawing{
|
|
||||||
buf: new(strings.Builder),
|
|
||||||
width: width,
|
|
||||||
}
|
|
||||||
p.drawTopBars(drawing.buf, labels...)
|
|
||||||
p.drawLabels(drawing.buf, labels...)
|
|
||||||
p.drawBottomBars(drawing.buf, labels...)
|
|
||||||
|
|
||||||
return drawing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw to writer
|
|
||||||
func (d *Drawing) Draw(writer io.Writer, centerOnWidth int) {
|
|
||||||
padSize := max((centerOnWidth-d.GetWidth())/2, 0)
|
|
||||||
for l := range strings.SplitSeq(d.buf.String(), "\n") {
|
|
||||||
if len(l) > 0 {
|
|
||||||
padding := strings.Repeat(" ", padSize)
|
|
||||||
fmt.Fprintf(writer, "%s%s\n", padding, l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWidth of drawing
|
|
||||||
func (d *Drawing) GetWidth() int {
|
|
||||||
return d.width
|
|
||||||
}
|
|
||||||
@@ -12,24 +12,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Warning that implements `error` but safe to ignore
|
|
||||||
type Warning struct {
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error the contract for error
|
|
||||||
func (w Warning) Error() string {
|
|
||||||
return w.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warningf create a warning
|
|
||||||
func Warningf(format string, args ...any) Warning {
|
|
||||||
w := Warning{
|
|
||||||
Message: fmt.Sprintf(format, args...),
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executor define contract for the steps of a workflow
|
// Executor define contract for the steps of a workflow
|
||||||
type Executor func(ctx context.Context) error
|
type Executor func(ctx context.Context) error
|
||||||
|
|
||||||
@@ -97,6 +79,12 @@ func NewErrorExecutor(err error) Executor {
|
|||||||
|
|
||||||
// NewParallelExecutor creates a new executor from a parallel of other executors
|
// NewParallelExecutor creates a new executor from a parallel of other executors
|
||||||
func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
||||||
|
if len(executors) == 0 {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
work := make(chan Executor, len(executors))
|
work := make(chan Executor, len(executors))
|
||||||
errs := make(chan error, len(executors))
|
errs := make(chan error, len(executors))
|
||||||
@@ -156,15 +144,9 @@ func NewParallelExecutor(parallel int, executors ...Executor) Executor {
|
|||||||
// Then runs another executor if this executor succeeds
|
// Then runs another executor if this executor succeeds
|
||||||
func (e Executor) Then(then Executor) Executor {
|
func (e Executor) Then(then Executor) Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
err := e(ctx)
|
if err := e(ctx); err != nil {
|
||||||
if err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case Warning:
|
|
||||||
Logger(ctx).Warning(err.Error())
|
|
||||||
default:
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewWorkflow(t *testing.T) {
|
func TestNewWorkflow(t *testing.T) {
|
||||||
@@ -119,6 +120,19 @@ func TestNewParallelExecutor(t *testing.T) {
|
|||||||
assert.NoError(errSingle)
|
assert.NoError(errSingle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewParallelExecutorEmpty(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
require.NoError(t, NewParallelExecutor(2)(ctx))
|
||||||
|
|
||||||
|
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
err := NewParallelExecutor(2)(canceledCtx)
|
||||||
|
assert.ErrorIs(err, context.Canceled)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewParallelExecutorFailed(t *testing.T) {
|
func TestNewParallelExecutorFailed(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2020 The nektos/act Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CopyFile copy file
|
|
||||||
func CopyFile(source, dest string) (err error) {
|
|
||||||
sourcefile, err := os.Open(source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer sourcefile.Close()
|
|
||||||
|
|
||||||
destfile, err := os.Create(dest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer destfile.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(destfile, sourcefile)
|
|
||||||
if err == nil {
|
|
||||||
sourceinfo, err := os.Stat(source)
|
|
||||||
if err != nil {
|
|
||||||
_ = os.Chmod(dest, sourceinfo.Mode())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CopyDir recursive copy of directory
|
|
||||||
func CopyDir(source, dest string) (err error) {
|
|
||||||
// get properties of source dir
|
|
||||||
sourceinfo, err := os.Stat(source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// create dest dir
|
|
||||||
|
|
||||||
err = os.MkdirAll(dest, sourceinfo.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
objects, err := os.ReadDir(source)
|
|
||||||
|
|
||||||
for _, obj := range objects {
|
|
||||||
sourcefilepointer := source + "/" + obj.Name()
|
|
||||||
|
|
||||||
destinationfilepointer := dest + "/" + obj.Name()
|
|
||||||
|
|
||||||
if obj.IsDir() {
|
|
||||||
// create sub-directories - recursively
|
|
||||||
err = CopyDir(sourcefilepointer, destinationfilepointer)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// perform copy
|
|
||||||
err = CopyFile(sourcefilepointer, destinationfilepointer)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err) //nolint:forbidigo // pre-existing issue from nektos/act
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -38,9 +38,11 @@ var (
|
|||||||
ErrNoRepo = errors.New("unable to find git repo")
|
ErrNoRepo = errors.New("unable to find git repo")
|
||||||
)
|
)
|
||||||
|
|
||||||
// acquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
|
// AcquireCloneLock returns an unlock function after locking the per-directory mutex for dir.
|
||||||
// Only concurrent operations targeting the same directory are erialized; clones into different directories run in parallel.
|
// Only concurrent operations targeting the same directory are serialized; clones into different directories run in parallel.
|
||||||
func acquireCloneLock(dir string) func() {
|
// Callers reading files inside dir (e.g. tarring a checked-out action into a job container) must hold this lock too,
|
||||||
|
// otherwise a concurrent NewGitCloneExecutor on the same dir can mutate the worktree mid-read.
|
||||||
|
func AcquireCloneLock(dir string) func() {
|
||||||
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
v, _ := cloneLocks.LoadOrStore(dir, &sync.Mutex{})
|
||||||
mu := v.(*sync.Mutex)
|
mu := v.(*sync.Mutex)
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -241,10 +243,14 @@ type NewGitCloneExecutorInput struct {
|
|||||||
InsecureSkipTLS bool
|
InsecureSkipTLS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloneIfRequired ...
|
// CloneIfRequired returns the repository and a boolean indicating whether an existing local clone was reused.
|
||||||
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, error) {
|
func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input NewGitCloneExecutorInput, logger log.FieldLogger) (*git.Repository, bool, error) {
|
||||||
r, err := git.PlainOpen(input.Dir)
|
r, err := git.PlainOpen(input.Dir)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
|
// Reuse existing clone
|
||||||
|
return r, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
var progressWriter io.Writer
|
var progressWriter io.Writer
|
||||||
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||||
if entry, ok := logger.(*log.Entry); ok {
|
if entry, ok := logger.(*log.Entry); ok {
|
||||||
@@ -273,15 +279,14 @@ func CloneIfRequired(ctx context.Context, refName plumbing.ReferenceName, input
|
|||||||
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
|
r, err = git.PlainCloneContext(ctx, input.Dir, false, &cloneOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
|
logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
if err = os.Chmod(input.Dir, 0o755); err != nil {
|
||||||
return nil, err
|
return nil, false, err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.PullOptions) {
|
||||||
@@ -305,13 +310,13 @@ func gitOptions(token string) (fetchOptions git.FetchOptions, pullOptions git.Pu
|
|||||||
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Infof(" \u2601 git clone '%s' # ref=%s", input.URL, input.Ref)
|
logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
|
||||||
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
||||||
|
|
||||||
defer acquireCloneLock(input.Dir)()
|
defer AcquireCloneLock(input.Dir)()
|
||||||
|
|
||||||
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
refName := plumbing.ReferenceName("refs/heads/" + input.Ref)
|
||||||
r, err := CloneIfRequired(ctx, refName, input, logger)
|
r, reused, err := CloneIfRequired(ctx, refName, input, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -336,10 +341,10 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
|||||||
var hash *plumbing.Hash
|
var hash *plumbing.Hash
|
||||||
rev := plumbing.Revision(input.Ref)
|
rev := plumbing.Revision(input.Ref)
|
||||||
if hash, err = r.ResolveRevision(rev); err != nil {
|
if hash, err = r.ResolveRevision(rev); err != nil {
|
||||||
|
// ResolveRevision returns a nil hash on error, and a branch ref legitimately fails
|
||||||
|
// here (no local refs/heads/<ref>); the duck-typing below resolves it.
|
||||||
logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
|
logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
|
||||||
}
|
} else if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
|
||||||
|
|
||||||
if hash.String() != input.Ref && strings.HasPrefix(hash.String(), input.Ref) {
|
|
||||||
return &Error{
|
return &Error{
|
||||||
err: ErrShortRef,
|
err: ErrShortRef,
|
||||||
commit: hash.String(),
|
commit: hash.String(),
|
||||||
@@ -390,12 +395,18 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) common.Executor {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reusedMsg := ""
|
||||||
|
|
||||||
if !isOfflineMode {
|
if !isOfflineMode {
|
||||||
if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
|
if err = w.Pull(&pullOptions); err != nil && err != git.NoErrAlreadyUpToDate {
|
||||||
logger.Debugf("Unable to pull %s: %v", refName, err)
|
logger.Debugf("Unable to pull %s: %v", refName, err)
|
||||||
}
|
}
|
||||||
|
} else if reused {
|
||||||
|
reusedMsg = " (reused in offline mode)"
|
||||||
}
|
}
|
||||||
logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
|
|
||||||
|
logger.Debugf("Cloned %s to %s%s", input.URL, input.Dir, reusedMsg)
|
||||||
|
|
||||||
if hash.String() != input.Ref && refType == "branch" {
|
if hash.String() != input.Ref && refType == "branch" {
|
||||||
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
|
logger.Debugf("Provided ref is not a sha. Updating branch ref after pull")
|
||||||
|
|||||||
@@ -279,6 +279,54 @@ func TestGitCloneExecutorNonFastForwardRef(t *testing.T) {
|
|||||||
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
|
assert.Equal(t, "second", strings.TrimSpace(string(out)), "working tree should be at the latest commit")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGitCloneExecutorOfflineMode(t *testing.T) {
|
||||||
|
gitConfig()
|
||||||
|
|
||||||
|
// Build a local "remote" with a single commit on main.
|
||||||
|
remoteDir := t.TempDir()
|
||||||
|
require.NoError(t, gitCmd("init", "--bare", "--initial-branch=main", remoteDir))
|
||||||
|
workDir := t.TempDir()
|
||||||
|
require.NoError(t, gitCmd("clone", remoteDir, workDir))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "checkout", "-b", "main"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "commit", "--allow-empty", "-m", "initial"))
|
||||||
|
require.NoError(t, gitCmd("-C", workDir, "push", "-u", "origin", "main"))
|
||||||
|
|
||||||
|
// Prime the cache with an online clone of main.
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
require.NoError(t, NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
|
URL: remoteDir,
|
||||||
|
Ref: "main",
|
||||||
|
Dir: cacheDir,
|
||||||
|
})(context.Background()))
|
||||||
|
|
||||||
|
t.Run("cached branch resolves without fetching", func(t *testing.T) {
|
||||||
|
// Offline reuse of a cached branch must succeed even though ResolveRevision(input.Ref)
|
||||||
|
// finds no local refs/heads/<ref>.
|
||||||
|
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
|
URL: remoteDir,
|
||||||
|
Ref: "main",
|
||||||
|
Dir: cacheDir,
|
||||||
|
OfflineMode: true,
|
||||||
|
})(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
out, err := exec.Command("git", "-C", cacheDir, "log", "--oneline", "-1", "--format=%s").Output()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "initial", strings.TrimSpace(string(out)))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unresolvable cached ref returns error", func(t *testing.T) {
|
||||||
|
// The ref was never cached; offline mode cannot resolve it and must return an error.
|
||||||
|
err := NewGitCloneExecutor(NewGitCloneExecutorInput{
|
||||||
|
URL: remoteDir,
|
||||||
|
Ref: "never-fetched",
|
||||||
|
Dir: cacheDir,
|
||||||
|
OfflineMode: true,
|
||||||
|
})(context.Background())
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func gitConfig() {
|
func gitConfig() {
|
||||||
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
if os.Getenv("GITHUB_ACTIONS") == "true" {
|
||||||
var err error
|
var err error
|
||||||
@@ -310,11 +358,11 @@ func TestAcquireCloneLock(t *testing.T) {
|
|||||||
t.Run("same directory serializes", func(t *testing.T) {
|
t.Run("same directory serializes", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
unlock1 := acquireCloneLock(dir)
|
unlock1 := AcquireCloneLock(dir)
|
||||||
|
|
||||||
secondAcquired := make(chan struct{})
|
secondAcquired := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
unlock := acquireCloneLock(dir)
|
unlock := AcquireCloneLock(dir)
|
||||||
close(secondAcquired)
|
close(secondAcquired)
|
||||||
unlock()
|
unlock()
|
||||||
}()
|
}()
|
||||||
@@ -338,12 +386,12 @@ func TestAcquireCloneLock(t *testing.T) {
|
|||||||
dirA := t.TempDir()
|
dirA := t.TempDir()
|
||||||
dirB := t.TempDir()
|
dirB := t.TempDir()
|
||||||
|
|
||||||
unlockA := acquireCloneLock(dirA)
|
unlockA := AcquireCloneLock(dirA)
|
||||||
defer unlockA()
|
defer unlockA()
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
unlock := acquireCloneLock(dirB)
|
unlock := AcquireCloneLock(dirB)
|
||||||
unlock()
|
unlock()
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type NewContainerInput struct {
|
|||||||
// Gitea specific
|
// Gitea specific
|
||||||
AutoRemove bool
|
AutoRemove bool
|
||||||
ValidVolumes []string
|
ValidVolumes []string
|
||||||
|
AllocatePTY bool // allocate a pseudo-TTY for the container's exec processes
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileEntry is a file to copy to a container
|
// FileEntry is a file to copy to a container
|
||||||
|
|||||||
@@ -8,34 +8,38 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
"github.com/docker/cli/cli/config/credentials"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/moby/moby/api/types/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfig, error) {
|
func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfig, error) {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
config, err := config.Load(config.Dir())
|
// config.LoadDefaultConfigFile panics on nil io.Writer when the config
|
||||||
|
// file is malformed; use config.Load to route errors through the logger.
|
||||||
|
cfg, err := config.Load(config.Dir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warnf("Could not load docker config: %v", err)
|
logger.Warnf("Could not load docker config: %v", err)
|
||||||
return registry.AuthConfig{}, err
|
return registry.AuthConfig{}, err
|
||||||
}
|
}
|
||||||
|
if !cfg.ContainsAuth() {
|
||||||
if !config.ContainsAuth() {
|
cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore)
|
||||||
config.CredentialsStore = credentials.DetectDefaultStore(config.CredentialsStore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hostName := "index.docker.io"
|
registryKey := registryAuthConfigKey("docker.io")
|
||||||
index := strings.IndexRune(image, '/')
|
if image != "" {
|
||||||
if index > -1 && (strings.ContainsAny(image[:index], ".:") || image[:index] == "localhost") {
|
if registryRef, refErr := reference.ParseNormalizedNamed(image); refErr != nil {
|
||||||
hostName = image[:index]
|
logger.Warnf("Could not normalize image reference: %v", refErr)
|
||||||
|
} else {
|
||||||
|
registryKey = registryAuthConfigKey(reference.Domain(registryRef))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authConfig, err := config.GetAuthConfig(hostName)
|
authConfig, err := cfg.GetAuthConfig(registryKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warnf("Could not get auth config from docker config: %v", err)
|
logger.Warnf("Could not get auth config from docker config: %v", err)
|
||||||
return registry.AuthConfig{}, err
|
return registry.AuthConfig{}, err
|
||||||
@@ -46,17 +50,20 @@ func LoadDockerAuthConfig(ctx context.Context, image string) (registry.AuthConfi
|
|||||||
|
|
||||||
func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig {
|
func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
config, err := config.Load(config.Dir())
|
cfg, err := config.Load(config.Dir())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warnf("Could not load docker config: %v", err)
|
logger.Warnf("Could not load docker config: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if !cfg.ContainsAuth() {
|
||||||
if !config.ContainsAuth() {
|
cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore)
|
||||||
config.CredentialsStore = credentials.DetectDefaultStore(config.CredentialsStore)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
creds, _ := config.GetAllCredentials()
|
creds, err := cfg.GetAllCredentials()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Could not get docker auth configs: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
authConfigs := make(map[string]registry.AuthConfig, len(creds))
|
authConfigs := make(map[string]registry.AuthConfig, len(creds))
|
||||||
for k, v := range creds {
|
for k, v := range creds {
|
||||||
authConfigs[k] = registry.AuthConfig(v)
|
authConfigs[k] = registry.AuthConfig(v)
|
||||||
@@ -64,3 +71,10 @@ func LoadDockerAuthConfigs(ctx context.Context) map[string]registry.AuthConfig {
|
|||||||
|
|
||||||
return authConfigs
|
return authConfigs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registryAuthConfigKey(domainName string) string {
|
||||||
|
if domainName == "docker.io" || domainName == "index.docker.io" {
|
||||||
|
return "https://index.docker.io/v1/"
|
||||||
|
}
|
||||||
|
return domainName
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ import (
|
|||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/moby/go-archive"
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/moby/go-archive/compression"
|
||||||
"github.com/moby/buildkit/frontend/dockerfile/dockerignore"
|
"github.com/moby/moby/client"
|
||||||
"github.com/moby/patternmatcher"
|
"github.com/moby/patternmatcher"
|
||||||
|
"github.com/moby/patternmatcher/ignorefile"
|
||||||
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDockerBuildExecutor function to create a run executor for the container
|
// NewDockerBuildExecutor function to create a run executor for the container
|
||||||
@@ -25,9 +27,9 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
if input.Platform != "" {
|
if input.Platform != "" {
|
||||||
logger.Infof("%sdocker build -t %s --platform %s %s", logPrefix, input.ImageTag, input.Platform, input.ContextDir)
|
logger.Infof("docker build -t %s --platform %s %s", input.ImageTag, input.Platform, input.ContextDir)
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("%sdocker build -t %s %s", logPrefix, input.ImageTag, input.ContextDir)
|
logger.Infof("docker build -t %s %s", input.ImageTag, input.ContextDir)
|
||||||
}
|
}
|
||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil
|
return nil
|
||||||
@@ -42,13 +44,19 @@ func NewDockerBuildExecutor(input NewDockerBuildExecutorInput) common.Executor {
|
|||||||
logger.Debugf("Building image from '%v'", input.ContextDir)
|
logger.Debugf("Building image from '%v'", input.ContextDir)
|
||||||
|
|
||||||
tags := []string{input.ImageTag}
|
tags := []string{input.ImageTag}
|
||||||
options := types.ImageBuildOptions{
|
options := client.ImageBuildOptions{
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
Remove: true,
|
Remove: true,
|
||||||
Platform: input.Platform,
|
|
||||||
AuthConfigs: LoadDockerAuthConfigs(ctx),
|
AuthConfigs: LoadDockerAuthConfigs(ctx),
|
||||||
Dockerfile: input.Dockerfile,
|
Dockerfile: input.Dockerfile,
|
||||||
}
|
}
|
||||||
|
platform, err := parsePlatform(input.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if platform != nil {
|
||||||
|
options.Platforms = []specs.Platform{*platform}
|
||||||
|
}
|
||||||
var buildContext io.ReadCloser
|
var buildContext io.ReadCloser
|
||||||
if input.BuildContext != nil {
|
if input.BuildContext != nil {
|
||||||
buildContext = io.NopCloser(input.BuildContext)
|
buildContext = io.NopCloser(input.BuildContext)
|
||||||
@@ -76,7 +84,7 @@ func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (
|
|||||||
common.Logger(ctx).Debugf("Creating archive for build context dir '%s' with relative dockerfile '%s'", contextDir, relDockerfile)
|
common.Logger(ctx).Debugf("Creating archive for build context dir '%s' with relative dockerfile '%s'", contextDir, relDockerfile)
|
||||||
|
|
||||||
// And canonicalize dockerfile name to a platform-independent one
|
// And canonicalize dockerfile name to a platform-independent one
|
||||||
relDockerfile = archive.CanonicalTarNameForPath(relDockerfile)
|
relDockerfile = filepath.ToSlash(relDockerfile)
|
||||||
|
|
||||||
f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
|
f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
@@ -86,7 +94,7 @@ func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (
|
|||||||
|
|
||||||
var excludes []string
|
var excludes []string
|
||||||
if err == nil {
|
if err == nil {
|
||||||
excludes, err = dockerignore.ReadAll(f) //nolint:staticcheck // pre-existing issue from nektos/act
|
excludes, err = ignorefile.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -106,9 +114,8 @@ func createBuildContext(ctx context.Context, contextDir, relDockerfile string) (
|
|||||||
includes = append(includes, ".dockerignore", relDockerfile)
|
includes = append(includes, ".dockerignore", relDockerfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
compression := archive.Uncompressed
|
|
||||||
buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{
|
buildCtx, err := archive.TarWithOptions(contextDir, &archive.TarOptions{
|
||||||
Compression: compression,
|
Compression: compression.None,
|
||||||
ExcludePatterns: excludes,
|
ExcludePatterns: excludes,
|
||||||
IncludeFiles: includes,
|
IncludeFiles: includes,
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,15 +16,18 @@ package container
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
networktypes "github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/moby/moby/api/types/container"
|
||||||
|
networktypes "github.com/moby/moby/api/types/network"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
@@ -77,21 +80,21 @@ func setupRunFlags() (*pflag.FlagSet, *containerOptions) {
|
|||||||
return flags, copts
|
return flags, copts
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) {
|
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
|
config, hostConfig, networkingConfig, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
return config, hostConfig
|
return config, hostConfig, networkingConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseRunLinks(t *testing.T) {
|
func TestParseRunLinks(t *testing.T) {
|
||||||
if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
|
if _, hostConfig, _ := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" {
|
||||||
t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
|
t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links)
|
||||||
}
|
}
|
||||||
if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
|
if _, hostConfig, _ := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" {
|
||||||
t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
|
t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links)
|
||||||
}
|
}
|
||||||
if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 {
|
if _, hostConfig, _ := mustParse(t, ""); len(hostConfig.Links) != 0 {
|
||||||
t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
|
t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +143,7 @@ func TestParseRunAttach(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.input, func(t *testing.T) {
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
config, _ := mustParse(t, tc.input)
|
config, _, _ := mustParse(t, tc.input)
|
||||||
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
|
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
|
||||||
assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout)
|
assert.Equal(t, config.AttachStdout, tc.expected.AttachStdout)
|
||||||
assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr)
|
assert.Equal(t, config.AttachStderr, tc.expected.AttachStderr)
|
||||||
@@ -194,10 +197,10 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseWithVolumes(t *testing.T) {
|
func TestParseWithVolumes(t *testing.T) { //nolint:gocyclo // verbatim copy from docker/cli tests
|
||||||
// A single volume
|
// A single volume
|
||||||
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
||||||
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
|
||||||
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
||||||
} else if _, exists := config.Volumes[arr[0]]; !exists {
|
} else if _, exists := config.Volumes[arr[0]]; !exists {
|
||||||
t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
|
t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes)
|
||||||
@@ -205,7 +208,7 @@ func TestParseWithVolumes(t *testing.T) {
|
|||||||
|
|
||||||
// Two volumes
|
// Two volumes
|
||||||
arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
|
arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`})
|
||||||
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil {
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds != nil {
|
||||||
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds)
|
||||||
} else if _, exists := config.Volumes[arr[0]]; !exists {
|
} else if _, exists := config.Volumes[arr[0]]; !exists {
|
||||||
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
|
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes)
|
||||||
@@ -215,13 +218,13 @@ func TestParseWithVolumes(t *testing.T) {
|
|||||||
|
|
||||||
// A single bind mount
|
// A single bind mount
|
||||||
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
|
||||||
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
|
||||||
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
|
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two bind mounts.
|
// Two bind mounts.
|
||||||
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
|
||||||
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
||||||
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,26 +233,26 @@ func TestParseWithVolumes(t *testing.T) {
|
|||||||
arr, tryit = setupPlatformVolume(
|
arr, tryit = setupPlatformVolume(
|
||||||
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
|
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
|
||||||
[]string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
|
[]string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`})
|
||||||
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
||||||
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similar to previous test but with alternate modes which are only supported by Linux
|
// Similar to previous test but with alternate modes which are only supported by Linux
|
||||||
if runtime.GOOS != "windows" {
|
if runtime.GOOS != "windows" {
|
||||||
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{})
|
||||||
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
||||||
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
||||||
}
|
}
|
||||||
|
|
||||||
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{})
|
||||||
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
if _, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
||||||
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One bind mount and one volume
|
// One bind mount and one volume
|
||||||
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
|
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`})
|
||||||
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] {
|
||||||
t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
|
t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds)
|
||||||
} else if _, exists := config.Volumes[arr[1]]; !exists {
|
} else if _, exists := config.Volumes[arr[1]]; !exists {
|
||||||
t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
|
t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes)
|
||||||
@@ -258,7 +261,7 @@ func TestParseWithVolumes(t *testing.T) {
|
|||||||
// Root to non-c: drive letter (Windows specific)
|
// Root to non-c: drive letter (Windows specific)
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
|
arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`})
|
||||||
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
|
if config, hostConfig, _ := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 {
|
||||||
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
|
t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,6 +297,36 @@ func compareRandomizedStrings(a, b, c, d string) error {
|
|||||||
return errors.Errorf("strings don't match")
|
return errors.Errorf("strings don't match")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustNetworkPort(t *testing.T, value string) networktypes.Port {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
port, err := networktypes.ParsePort(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse network port %q: %v", value, err)
|
||||||
|
}
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustAddr(t *testing.T, value string) netip.Addr {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
addr, err := netip.ParseAddr(value)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse address %q: %v", value, err)
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustAddrs(t *testing.T, values ...string) []netip.Addr {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
addrs := make([]netip.Addr, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
addrs = append(addrs, mustAddr(t, value))
|
||||||
|
}
|
||||||
|
return addrs
|
||||||
|
}
|
||||||
|
|
||||||
// Simple parse with MacAddress validation
|
// Simple parse with MacAddress validation
|
||||||
func TestParseWithMacAddress(t *testing.T) {
|
func TestParseWithMacAddress(t *testing.T) {
|
||||||
invalidMacAddress := "--mac-address=invalidMacAddress"
|
invalidMacAddress := "--mac-address=invalidMacAddress"
|
||||||
@@ -301,9 +334,10 @@ func TestParseWithMacAddress(t *testing.T) {
|
|||||||
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
|
if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" {
|
||||||
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
|
t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err)
|
||||||
}
|
}
|
||||||
if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // pre-existing issue from nektos/act
|
_, hostConfig, networkingConfig := mustParse(t, validMacAddress)
|
||||||
t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) //nolint:staticcheck // pre-existing issue from nektos/act
|
endpoint := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)]
|
||||||
}
|
assert.Check(t, endpoint != nil)
|
||||||
|
assert.Equal(t, "92:d0:c6:0a:29:33", endpoint.MacAddress.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunFlagsParseWithMemory(t *testing.T) {
|
func TestRunFlagsParseWithMemory(t *testing.T) {
|
||||||
@@ -312,7 +346,7 @@ func TestRunFlagsParseWithMemory(t *testing.T) {
|
|||||||
err := flags.Parse(args)
|
err := flags.Parse(args)
|
||||||
assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`)
|
assert.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`)
|
||||||
|
|
||||||
_, hostconfig := mustParse(t, "--memory=1G")
|
_, hostconfig, _ := mustParse(t, "--memory=1G")
|
||||||
assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory))
|
assert.Check(t, is.Equal(int64(1073741824), hostconfig.Memory))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,10 +356,10 @@ func TestParseWithMemorySwap(t *testing.T) {
|
|||||||
err := flags.Parse(args)
|
err := flags.Parse(args)
|
||||||
assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`)
|
assert.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`)
|
||||||
|
|
||||||
_, hostconfig := mustParse(t, "--memory-swap=1G")
|
_, hostconfig, _ := mustParse(t, "--memory-swap=1G")
|
||||||
assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap))
|
assert.Check(t, is.Equal(int64(1073741824), hostconfig.MemorySwap))
|
||||||
|
|
||||||
_, hostconfig = mustParse(t, "--memory-swap=-1")
|
_, hostconfig, _ = mustParse(t, "--memory-swap=-1")
|
||||||
assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap))
|
assert.Check(t, is.Equal(int64(-1), hostconfig.MemorySwap))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,14 +374,14 @@ func TestParseHostname(t *testing.T) {
|
|||||||
hostnameWithDomain := "--hostname=hostname.domainname"
|
hostnameWithDomain := "--hostname=hostname.domainname"
|
||||||
hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
|
hostnameWithDomainTld := "--hostname=hostname.domainname.tld"
|
||||||
for hostname, expectedHostname := range validHostnames {
|
for hostname, expectedHostname := range validHostnames {
|
||||||
if config, _ := mustParse(t, "--hostname="+hostname); config.Hostname != expectedHostname {
|
if config, _, _ := mustParse(t, "--hostname="+hostname); config.Hostname != expectedHostname {
|
||||||
t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname)
|
t.Fatalf("Expected the config to have 'hostname' as %q, got %q", expectedHostname, config.Hostname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" {
|
if config, _, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" || config.Domainname != "" {
|
||||||
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname)
|
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got %q", config.Hostname)
|
||||||
}
|
}
|
||||||
if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" {
|
if config, _, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" || config.Domainname != "" {
|
||||||
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname)
|
t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got %q", config.Hostname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,26 +395,28 @@ func TestParseHostnameDomainname(t *testing.T) {
|
|||||||
"domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors",
|
"domainname-63-bytes-long-should-be-valid-and-without-any-errors": "domainname-63-bytes-long-should-be-valid-and-without-any-errors",
|
||||||
}
|
}
|
||||||
for domainname, expectedDomainname := range validDomainnames {
|
for domainname, expectedDomainname := range validDomainnames {
|
||||||
if config, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname {
|
if config, _, _ := mustParse(t, "--domainname="+domainname); config.Domainname != expectedDomainname {
|
||||||
t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname)
|
t.Fatalf("Expected the config to have 'domainname' as %q, got %q", expectedDomainname, config.Domainname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if config, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" {
|
if config, _, _ := mustParse(t, "--hostname=some.prefix --domainname=domainname"); config.Hostname != "some.prefix" || config.Domainname != "domainname" {
|
||||||
t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname)
|
t.Fatalf("Expected the config to have 'hostname' as 'some.prefix' and 'domainname' as 'domainname', got %q and %q", config.Hostname, config.Domainname)
|
||||||
}
|
}
|
||||||
if config, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" {
|
if config, _, _ := mustParse(t, "--hostname=another-prefix --domainname=domainname.tld"); config.Hostname != "another-prefix" || config.Domainname != "domainname.tld" {
|
||||||
t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname)
|
t.Fatalf("Expected the config to have 'hostname' as 'another-prefix' and 'domainname' as 'domainname.tld', got %q and %q", config.Hostname, config.Domainname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseWithExpose(t *testing.T) {
|
func TestParseWithExpose(t *testing.T) {
|
||||||
invalids := map[string]string{
|
invalids := []string{
|
||||||
":": "invalid port format for --expose: :",
|
":",
|
||||||
"8080:9090": "invalid port format for --expose: 8080:9090",
|
"8080:9090",
|
||||||
"NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
|
"/tcp",
|
||||||
"NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
|
"/udp",
|
||||||
"8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`,
|
"NaN/tcp",
|
||||||
"1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`,
|
"NaN-NaN/tcp",
|
||||||
|
"8080-NaN/tcp",
|
||||||
|
"1234567890-8080/tcp",
|
||||||
}
|
}
|
||||||
valids := map[string][]nat.Port{
|
valids := map[string][]nat.Port{
|
||||||
"8080/tcp": {"8080/tcp"},
|
"8080/tcp": {"8080/tcp"},
|
||||||
@@ -389,9 +425,9 @@ func TestParseWithExpose(t *testing.T) {
|
|||||||
"8080-8080/udp": {"8080/udp"},
|
"8080-8080/udp": {"8080/udp"},
|
||||||
"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
|
"8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"},
|
||||||
}
|
}
|
||||||
for expose, expectedError := range invalids {
|
for _, expose := range invalids {
|
||||||
if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError {
|
if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil {
|
||||||
t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err)
|
t.Fatalf("Expected error with '--expose=%v', got none", expose)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for expose, exposedPorts := range valids {
|
for expose, exposedPorts := range valids {
|
||||||
@@ -403,7 +439,7 @@ func TestParseWithExpose(t *testing.T) {
|
|||||||
t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
|
t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts))
|
||||||
}
|
}
|
||||||
for _, port := range exposedPorts {
|
for _, port := range exposedPorts {
|
||||||
if _, ok := config.ExposedPorts[port]; !ok {
|
if _, ok := config.ExposedPorts[mustNetworkPort(t, string(port))]; !ok {
|
||||||
t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
|
t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,7 +454,7 @@ func TestParseWithExpose(t *testing.T) {
|
|||||||
}
|
}
|
||||||
ports := []nat.Port{"80/tcp", "81/tcp"}
|
ports := []nat.Port{"80/tcp", "81/tcp"}
|
||||||
for _, port := range ports {
|
for _, port := range ports {
|
||||||
if _, ok := config.ExposedPorts[port]; !ok {
|
if _, ok := config.ExposedPorts[mustNetworkPort(t, string(port))]; !ok {
|
||||||
t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
|
t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -498,9 +534,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
|||||||
expected: map[string]*networktypes.EndpointSettings{
|
expected: map[string]*networktypes.EndpointSettings{
|
||||||
"net1": {
|
"net1": {
|
||||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||||
IPv4Address: "172.20.88.22",
|
IPv4Address: mustAddr(t, "172.20.88.22"),
|
||||||
IPv6Address: "2001:db8::8822",
|
IPv6Address: mustAddr(t, "2001:db8::8822"),
|
||||||
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
|
LinkLocalIPs: mustAddrs(t, "169.254.2.2", "fe80::169:254:2:2"),
|
||||||
},
|
},
|
||||||
Links: []string{"foo:bar", "bar:baz"},
|
Links: []string{"foo:bar", "bar:baz"},
|
||||||
Aliases: []string{"web1", "web2"},
|
Aliases: []string{"web1", "web2"},
|
||||||
@@ -527,9 +563,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
|||||||
"net1": {
|
"net1": {
|
||||||
DriverOpts: map[string]string{"field1": "value1"},
|
DriverOpts: map[string]string{"field1": "value1"},
|
||||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||||
IPv4Address: "172.20.88.22",
|
IPv4Address: mustAddr(t, "172.20.88.22"),
|
||||||
IPv6Address: "2001:db8::8822",
|
IPv6Address: mustAddr(t, "2001:db8::8822"),
|
||||||
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
|
LinkLocalIPs: mustAddrs(t, "169.254.2.2", "fe80::169:254:2:2"),
|
||||||
},
|
},
|
||||||
Links: []string{"foo:bar", "bar:baz"},
|
Links: []string{"foo:bar", "bar:baz"},
|
||||||
Aliases: []string{"web1", "web2"},
|
Aliases: []string{"web1", "web2"},
|
||||||
@@ -538,8 +574,8 @@ func TestParseNetworkConfig(t *testing.T) {
|
|||||||
"net3": {
|
"net3": {
|
||||||
DriverOpts: map[string]string{"field3": "value3"},
|
DriverOpts: map[string]string{"field3": "value3"},
|
||||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||||
IPv4Address: "172.20.88.22",
|
IPv4Address: mustAddr(t, "172.20.88.22"),
|
||||||
IPv6Address: "2001:db8::8822",
|
IPv6Address: mustAddr(t, "2001:db8::8822"),
|
||||||
},
|
},
|
||||||
Aliases: []string{"web3"},
|
Aliases: []string{"web3"},
|
||||||
},
|
},
|
||||||
@@ -556,8 +592,8 @@ func TestParseNetworkConfig(t *testing.T) {
|
|||||||
"field2": "value2",
|
"field2": "value2",
|
||||||
},
|
},
|
||||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||||
IPv4Address: "172.20.88.22",
|
IPv4Address: mustAddr(t, "172.20.88.22"),
|
||||||
IPv6Address: "2001:db8::8822",
|
IPv6Address: mustAddr(t, "2001:db8::8822"),
|
||||||
},
|
},
|
||||||
Aliases: []string{"web1", "web2"},
|
Aliases: []string{"web1", "web2"},
|
||||||
},
|
},
|
||||||
@@ -610,7 +646,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
|||||||
|
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedCfg.NetworkMode)
|
assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedCfg.NetworkMode)
|
||||||
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected)
|
if diff := cmp.Diff(tc.expected, nwConfig.EndpointsConfig, cmpopts.EquateComparable(netip.Addr{})); diff != "" {
|
||||||
|
t.Fatalf("unexpected endpoints (-want +got):\n%s", diff)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -631,7 +669,7 @@ func TestParseModes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// uts ko
|
// uts ko
|
||||||
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"})
|
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"}) //nolint:dogsled // verbatim copy from docker/cli tests
|
||||||
assert.ErrorContains(t, err, "--uts: invalid UTS mode")
|
assert.ErrorContains(t, err, "--uts: invalid UTS mode")
|
||||||
|
|
||||||
// uts ok
|
// uts ok
|
||||||
@@ -691,10 +729,9 @@ func TestParseRestartPolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseRestartPolicyAutoRemove(t *testing.T) {
|
func TestParseRestartPolicyAutoRemove(t *testing.T) {
|
||||||
expected := "Conflicting options: --restart and --rm"
|
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) //nolint:dogsled // verbatim copy from docker/cli tests
|
||||||
_, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"})
|
if err == nil {
|
||||||
if err == nil || err.Error() != expected {
|
t.Fatal("Expected error for conflicting --restart and --rm, but got none")
|
||||||
t.Fatalf("Expected error %v, but got none", expected)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,7 +789,7 @@ func TestParseLoggingOpts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseEnvfileVariables(t *testing.T) { //nolint:dupl // pre-existing issue from nektos/act
|
func TestParseEnvfileVariables(t *testing.T) { //nolint:dupl // verbatim copy from docker/cli tests
|
||||||
e := "open nonexistent: no such file or directory"
|
e := "open nonexistent: no such file or directory"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
e = "open nonexistent: The system cannot find the file specified."
|
e = "open nonexistent: The system cannot find the file specified."
|
||||||
@@ -795,7 +832,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UTF16 with BOM
|
// UTF16 with BOM
|
||||||
e := "contains invalid utf8 bytes at line"
|
e := "invalid env file"
|
||||||
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
|
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
|
||||||
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
||||||
}
|
}
|
||||||
@@ -805,7 +842,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseLabelfileVariables(t *testing.T) { //nolint:dupl // pre-existing issue from nektos/act
|
func TestParseLabelfileVariables(t *testing.T) { //nolint:dupl // verbatim copy from docker/cli tests
|
||||||
e := "open nonexistent: no such file or directory"
|
e := "open nonexistent: no such file or directory"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
e = "open nonexistent: The system cannot find the file specified."
|
e = "open nonexistent: The system cannot find the file specified."
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
cerrdefs "github.com/containerd/errdefs"
|
||||||
"github.com/docker/docker/client"
|
"github.com/moby/moby/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ImageExistsLocally returns a boolean indicating if an image with the
|
// ImageExistsLocally returns a boolean indicating if an image with the
|
||||||
@@ -23,8 +23,8 @@ func ImageExistsLocally(ctx context.Context, imageName, platform string) (bool,
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
|
inspectImage, err := cli.ImageInspect(ctx, imageName)
|
||||||
if client.IsErrNotFound(err) {
|
if cerrdefs.IsNotFound(err) {
|
||||||
return false, nil
|
return false, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -46,14 +46,14 @@ func RemoveImage(ctx context.Context, imageName string, force, pruneChildren boo
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
inspectImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
|
inspectImage, err := cli.ImageInspect(ctx, imageName)
|
||||||
if client.IsErrNotFound(err) {
|
if cerrdefs.IsNotFound(err) {
|
||||||
return false, nil
|
return false, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = cli.ImageRemove(ctx, inspectImage.ID, types.ImageRemoveOptions{
|
if _, err = cli.ImageRemove(ctx, inspectImage.ID, client.ImageRemoveOptions{
|
||||||
Force: force,
|
Force: force,
|
||||||
PruneChildren: pruneChildren,
|
PruneChildren: pruneChildren,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/moby/moby/client"
|
||||||
"github.com/docker/docker/client"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -38,14 +38,14 @@ func TestImageExistsLocally(t *testing.T) {
|
|||||||
assert.False(t, invalidImagePlatform)
|
assert.False(t, invalidImagePlatform)
|
||||||
|
|
||||||
// pull an image
|
// pull an image
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
cli, err := client.New(client.FromEnv)
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
cli.NegotiateAPIVersion(context.Background())
|
defer cli.Close()
|
||||||
|
|
||||||
// Chose alpine latest because it's so small
|
// Chose alpine latest because it's so small
|
||||||
// maybe we should build an image instead so that tests aren't reliable on dockerhub
|
// maybe we should build an image instead so that tests aren't reliable on dockerhub
|
||||||
readerDefault, err := cli.ImagePull(ctx, "node:24-bookworm-slim", types.ImagePullOptions{
|
readerDefault, err := cli.ImagePull(ctx, "node:24-bookworm-slim", client.ImagePullOptions{
|
||||||
Platform: "linux/amd64",
|
Platforms: []specs.Platform{{OS: "linux", Architecture: "amd64"}},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
defer readerDefault.Close()
|
defer readerDefault.Close()
|
||||||
@@ -57,8 +57,8 @@ func TestImageExistsLocally(t *testing.T) {
|
|||||||
assert.True(t, imageDefaultArchExists)
|
assert.True(t, imageDefaultArchExists)
|
||||||
|
|
||||||
// Validate if another architecture platform can be pulled
|
// Validate if another architecture platform can be pulled
|
||||||
readerArm64, err := cli.ImagePull(ctx, "node:24-bookworm-slim", types.ImagePullOptions{
|
readerArm64, err := cli.ImagePull(ctx, "node:24-bookworm-slim", client.ImagePullOptions{
|
||||||
Platform: "linux/arm64",
|
Platforms: []specs.Platform{{OS: "linux", Architecture: "arm64"}},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
defer readerArm64.Close()
|
defer readerArm64.Close()
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ type dockerMessage struct {
|
|||||||
Progress string `json:"progress"`
|
Progress string `json:"progress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const logPrefix = " \U0001F433 "
|
|
||||||
|
|
||||||
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
|
func logDockerResponse(logger logrus.FieldLogger, dockerResponse io.ReadCloser, isError bool) error {
|
||||||
if dockerResponse == nil {
|
if dockerResponse == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/moby/moby/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDockerNetworkCreateExecutor(name string) common.Executor {
|
func NewDockerNetworkCreateExecutor(name string) common.Executor {
|
||||||
@@ -23,20 +23,20 @@ func NewDockerNetworkCreateExecutor(name string) common.Executor {
|
|||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
// Only create the network if it doesn't exist
|
// Only create the network if it doesn't exist
|
||||||
networks, err := cli.NetworkList(ctx, types.NetworkListOptions{})
|
networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// For Gitea, reduce log noise
|
// For Gitea, reduce log noise
|
||||||
// common.Logger(ctx).Debugf("%v", networks)
|
// common.Logger(ctx).Debugf("%v", networks)
|
||||||
for _, network := range networks {
|
for _, n := range networks.Items {
|
||||||
if network.Name == name {
|
if n.Name == name {
|
||||||
common.Logger(ctx).Debugf("Network %v exists", name)
|
common.Logger(ctx).Debugf("Network %v exists", name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = cli.NetworkCreate(ctx, name, types.NetworkCreate{
|
_, err = cli.NetworkCreate(ctx, name, client.NetworkCreateOptions{
|
||||||
Driver: "bridge",
|
Driver: "bridge",
|
||||||
Scope: "local",
|
Scope: "local",
|
||||||
})
|
})
|
||||||
@@ -56,23 +56,23 @@ func NewDockerNetworkRemoveExecutor(name string) common.Executor {
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
// Make shure that all network of the specified name are removed
|
// Make sure that all network of the specified name are removed
|
||||||
// cli.NetworkRemove refuses to remove a network if there are duplicates
|
// cli.NetworkRemove refuses to remove a network if there are duplicates
|
||||||
networks, err := cli.NetworkList(ctx, types.NetworkListOptions{})
|
networks, err := cli.NetworkList(ctx, client.NetworkListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// For Gitea, reduce log noise
|
// For Gitea, reduce log noise
|
||||||
// common.Logger(ctx).Debugf("%v", networks)
|
// common.Logger(ctx).Debugf("%v", networks)
|
||||||
for _, network := range networks {
|
for _, n := range networks.Items {
|
||||||
if network.Name == name {
|
if n.Name == name {
|
||||||
result, err := cli.NetworkInspect(ctx, network.ID, types.NetworkInspectOptions{})
|
result, err := cli.NetworkInspect(ctx, n.ID, client.NetworkInspectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Containers) == 0 {
|
if len(result.Network.Containers) == 0 {
|
||||||
if err = cli.NetworkRemove(ctx, network.ID); err != nil {
|
if _, err = cli.NetworkRemove(ctx, n.ID, client.NetworkRemoveOptions{}); err != nil {
|
||||||
common.Logger(ctx).Debugf("%v", err)
|
common.Logger(ctx).Debugf("%v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
39
act/container/docker_platform.go
Normal file
39
act/container/docker_platform.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2025 The nektos/act Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//go:build !(WITHOUT_DOCKER || !(linux || darwin || windows || netbsd))
|
||||||
|
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// parsePlatform parses an "os/arch[/variant]" string into a Platform. An empty input
|
||||||
|
// returns (nil, nil), meaning "no platform constraint". A non-empty but malformed
|
||||||
|
// string is rejected explicitly so it cannot silently fall through to the daemon's
|
||||||
|
// default architecture.
|
||||||
|
func parsePlatform(platform string) (*specs.Platform, error) {
|
||||||
|
if platform == "" {
|
||||||
|
return nil, nil //nolint:nilnil // no platform constraint requested
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(platform, "/")
|
||||||
|
if len(parts) < 2 || len(parts) > 3 || parts[0] == "" || parts[1] == "" || (len(parts) == 3 && parts[2] == "") {
|
||||||
|
return nil, fmt.Errorf("invalid platform %q: expected os/arch[/variant]", platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
spec := &specs.Platform{
|
||||||
|
OS: strings.ToLower(parts[0]),
|
||||||
|
Architecture: strings.ToLower(parts[1]),
|
||||||
|
}
|
||||||
|
if len(parts) == 3 {
|
||||||
|
spec.Variant = strings.ToLower(parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
63
act/container/docker_platform_test.go
Normal file
63
act/container/docker_platform_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParsePlatform(t *testing.T) {
|
||||||
|
t.Run("empty input returns nil platform without error", func(t *testing.T) {
|
||||||
|
got, err := parsePlatform("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("os/arch", func(t *testing.T) {
|
||||||
|
got, err := parsePlatform("linux/amd64")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "linux", got.OS)
|
||||||
|
assert.Equal(t, "amd64", got.Architecture)
|
||||||
|
assert.Empty(t, got.Variant)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("os/arch/variant", func(t *testing.T) {
|
||||||
|
got, err := parsePlatform("linux/arm/v7")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "linux", got.OS)
|
||||||
|
assert.Equal(t, "arm", got.Architecture)
|
||||||
|
assert.Equal(t, "v7", got.Variant)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("input is lowercased", func(t *testing.T) {
|
||||||
|
got, err := parsePlatform("Linux/AMD64/V8")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, "linux", got.OS)
|
||||||
|
assert.Equal(t, "amd64", got.Architecture)
|
||||||
|
assert.Equal(t, "v8", got.Variant)
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, bad := range []string{
|
||||||
|
"amd64",
|
||||||
|
"linux",
|
||||||
|
"linux/",
|
||||||
|
"/amd64",
|
||||||
|
"/",
|
||||||
|
"//",
|
||||||
|
"linux/arm/",
|
||||||
|
"linux/arm/v7/extra",
|
||||||
|
} {
|
||||||
|
t.Run("rejects "+bad, func(t *testing.T) {
|
||||||
|
got, err := parsePlatform(bad)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Nil(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,23 +8,23 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/moby/moby/api/pkg/authconfig"
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/moby/moby/api/types/registry"
|
||||||
|
"github.com/moby/moby/client"
|
||||||
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewDockerPullExecutor function to create a run executor for the container
|
// NewDockerPullExecutor function to create a run executor for the container
|
||||||
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Debugf("%sdocker pull %v", logPrefix, input.Image)
|
logger.Debugf("docker pull %v", input.Image)
|
||||||
|
|
||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil
|
return nil
|
||||||
@@ -78,26 +78,29 @@ func NewDockerPullExecutor(input NewDockerPullExecutorInput) common.Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (types.ImagePullOptions, error) {
|
func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput) (client.ImagePullOptions, error) {
|
||||||
imagePullOptions := types.ImagePullOptions{
|
imagePullOptions := client.ImagePullOptions{}
|
||||||
Platform: input.Platform,
|
platform, err := parsePlatform(input.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return imagePullOptions, err
|
||||||
|
}
|
||||||
|
if platform != nil {
|
||||||
|
imagePullOptions.Platforms = []specs.Platform{*platform}
|
||||||
}
|
}
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
if input.Username != "" && input.Password != "" {
|
if input.Username != "" && input.Password != "" {
|
||||||
logger.Debugf("using authentication for docker pull")
|
logger.Debugf("using authentication for docker pull")
|
||||||
|
|
||||||
authConfig := registry.AuthConfig{
|
encodedAuth, err := authconfig.Encode(registry.AuthConfig{
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
Password: input.Password,
|
Password: input.Password,
|
||||||
}
|
})
|
||||||
|
|
||||||
encodedJSON, err := json.Marshal(authConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imagePullOptions, err
|
return imagePullOptions, err
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
|
imagePullOptions.RegistryAuth = encodedAuth
|
||||||
} else {
|
} else {
|
||||||
authConfig, err := LoadDockerAuthConfig(ctx, input.Image)
|
authConfig, err := LoadDockerAuthConfig(ctx, input.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,19 +111,17 @@ func getImagePullOptions(ctx context.Context, input NewDockerPullExecutorInput)
|
|||||||
}
|
}
|
||||||
logger.Info("using DockerAuthConfig authentication for docker pull")
|
logger.Info("using DockerAuthConfig authentication for docker pull")
|
||||||
|
|
||||||
encodedJSON, err := json.Marshal(authConfig)
|
imagePullOptions.RegistryAuth, err = authconfig.Encode(authConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imagePullOptions, err
|
return imagePullOptions, err
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePullOptions.RegistryAuth = base64.URLEncoding.EncodeToString(encodedJSON)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return imagePullOptions, nil
|
return imagePullOptions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanImage(ctx context.Context, image string) string {
|
func cleanImage(ctx context.Context, imageName string) string {
|
||||||
ref, err := reference.ParseAnyReference(image)
|
ref, err := reference.ParseAnyReference(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Logger(ctx).Error(err)
|
common.Logger(ctx).Error(err)
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -17,31 +17,31 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/filecollector"
|
"gitea.com/gitea/runner/act/filecollector"
|
||||||
|
|
||||||
|
"dario.cat/mergo"
|
||||||
"github.com/Masterminds/semver"
|
"github.com/Masterminds/semver"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/docker/cli/cli/connhelper"
|
"github.com/docker/cli/cli/connhelper"
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/docker/docker/api/types/mount"
|
|
||||||
"github.com/docker/docker/api/types/network"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/docker/docker/pkg/stdcopy"
|
|
||||||
"github.com/go-git/go-billy/v5/helper/polyfill"
|
"github.com/go-git/go-billy/v5/helper/polyfill"
|
||||||
"github.com/go-git/go-billy/v5/osfs"
|
"github.com/go-git/go-billy/v5/osfs"
|
||||||
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
"github.com/imdario/mergo"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
|
"github.com/moby/moby/api/pkg/stdcopy"
|
||||||
|
"github.com/moby/moby/api/types/container"
|
||||||
|
"github.com/moby/moby/api/types/mount"
|
||||||
|
"github.com/moby/moby/api/types/network"
|
||||||
|
"github.com/moby/moby/api/types/system"
|
||||||
|
"github.com/moby/moby/client"
|
||||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewContainer creates a reference to a container
|
// NewContainer creates a reference to a container
|
||||||
@@ -53,7 +53,7 @@ func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
|
|||||||
|
|
||||||
func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
|
func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewDebugExecutor("%sdocker network connect %s %s", logPrefix, name, cr.input.Name).
|
NewDebugExecutor("docker network connect %s %s", name, cr.input.Name).
|
||||||
Then(
|
Then(
|
||||||
common.NewPipelineExecutor(
|
common.NewPipelineExecutor(
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
@@ -64,9 +64,13 @@ func (cr *containerReference) ConnectToNetwork(name string) common.Executor {
|
|||||||
|
|
||||||
func (cr *containerReference) connectToNetwork(name string, aliases []string) common.Executor {
|
func (cr *containerReference) connectToNetwork(name string, aliases []string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
return cr.cli.NetworkConnect(ctx, name, cr.input.Name, &network.EndpointSettings{
|
_, err := cr.cli.NetworkConnect(ctx, name, client.NetworkConnectOptions{
|
||||||
|
Container: cr.input.Name,
|
||||||
|
EndpointConfig: &network.EndpointSettings{
|
||||||
Aliases: aliases,
|
Aliases: aliases,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ func (cr *containerReference) connectToNetwork(name string, aliases []string) co
|
|||||||
// API version is 1.41 and beyond
|
// API version is 1.41 and beyond
|
||||||
func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) bool {
|
func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) bool {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
ver, err := cli.ServerVersion(ctx)
|
ver, err := cli.ServerVersion(ctx, client.ServerVersionOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Panicf("Failed to get Docker API Version: %s", err)
|
logger.Panicf("Failed to get Docker API Version: %s", err)
|
||||||
return false
|
return false
|
||||||
@@ -90,7 +94,7 @@ func supportsContainerImagePlatform(ctx context.Context, cli client.APIClient) b
|
|||||||
|
|
||||||
func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewInfoExecutor("%sdocker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
NewInfoExecutor("docker create image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||||
Then(
|
Then(
|
||||||
common.NewPipelineExecutor(
|
common.NewPipelineExecutor(
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
@@ -102,7 +106,7 @@ func (cr *containerReference) Create(capAdd, capDrop []string) common.Executor {
|
|||||||
|
|
||||||
func (cr *containerReference) Start(attach bool) common.Executor {
|
func (cr *containerReference) Start(attach bool) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewInfoExecutor("%sdocker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
NewInfoExecutor("docker run image=%s platform=%s entrypoint=%+q cmd=%+q network=%+q", cr.input.Image, cr.input.Platform, cr.input.Entrypoint, cr.input.Cmd, cr.input.NetworkMode).
|
||||||
Then(
|
Then(
|
||||||
common.NewPipelineExecutor(
|
common.NewPipelineExecutor(
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
@@ -125,7 +129,7 @@ func (cr *containerReference) Start(attach bool) common.Executor {
|
|||||||
|
|
||||||
func (cr *containerReference) Pull(forcePull bool) common.Executor {
|
func (cr *containerReference) Pull(forcePull bool) common.Executor {
|
||||||
return common.
|
return common.
|
||||||
NewInfoExecutor("%sdocker pull image=%s platform=%s username=%s forcePull=%t", logPrefix, cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
|
NewInfoExecutor("docker pull image=%s platform=%s username=%s forcePull=%t", cr.input.Image, cr.input.Platform, cr.input.Username, forcePull).
|
||||||
Then(
|
Then(
|
||||||
NewDockerPullExecutor(NewDockerPullExecutorInput{
|
NewDockerPullExecutor(NewDockerPullExecutorInput{
|
||||||
Image: cr.input.Image,
|
Image: cr.input.Image,
|
||||||
@@ -147,7 +151,7 @@ func (cr *containerReference) Copy(destPath string, files ...*FileEntry) common.
|
|||||||
|
|
||||||
func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
|
func (cr *containerReference) CopyDir(destPath, srcPath string, useGitIgnore bool) common.Executor {
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
common.NewInfoExecutor("%sdocker cp src=%s dst=%s", logPrefix, srcPath, destPath),
|
common.NewInfoExecutor("docker cp src=%s dst=%s", srcPath, destPath),
|
||||||
cr.copyDir(destPath, srcPath, useGitIgnore),
|
cr.copyDir(destPath, srcPath, useGitIgnore),
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
// If this fails, then folders have wrong permissions on non root container
|
// If this fails, then folders have wrong permissions on non root container
|
||||||
@@ -163,8 +167,11 @@ func (cr *containerReference) GetContainerArchive(ctx context.Context, srcPath s
|
|||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil, errors.New("DRYRUN is not supported in GetContainerArchive")
|
return nil, errors.New("DRYRUN is not supported in GetContainerArchive")
|
||||||
}
|
}
|
||||||
a, _, err := cr.cli.CopyFromContainer(ctx, cr.id, srcPath)
|
result, err := cr.cli.CopyFromContainer(ctx, cr.id, client.CopyFromContainerOptions{SourcePath: srcPath})
|
||||||
return a, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result.Content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
|
func (cr *containerReference) UpdateFromEnv(srcPath string, env *map[string]string) common.Executor {
|
||||||
@@ -177,7 +184,7 @@ func (cr *containerReference) UpdateFromImageEnv(env *map[string]string) common.
|
|||||||
|
|
||||||
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
func (cr *containerReference) Exec(command []string, env map[string]string, user, workdir string) common.Executor {
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
common.NewInfoExecutor("%sdocker exec cmd=[%s] user=%s workdir=%s", logPrefix, strings.Join(command, " "), user, workdir),
|
common.NewInfoExecutor("docker exec cmd=[%s] user=%s workdir=%s", strings.Join(command, " "), user, workdir),
|
||||||
cr.connect(),
|
cr.connect(),
|
||||||
cr.find(),
|
cr.find(),
|
||||||
cr.exec(command, env, user, workdir),
|
cr.exec(command, env, user, workdir),
|
||||||
@@ -222,22 +229,27 @@ func GetDockerClient(ctx context.Context) (cli client.APIClient, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cli, err = client.NewClientWithOpts(
|
cli, err = client.New(
|
||||||
client.WithHost(helper.Host),
|
client.WithHost(helper.Host),
|
||||||
client.WithDialContext(helper.Dialer),
|
client.WithDialContext(helper.Dialer),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
cli, err = client.NewClientWithOpts(client.FromEnv)
|
cli, err = client.New(client.FromEnv)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to docker daemon: %w", err)
|
return nil, fmt.Errorf("failed to connect to docker daemon: %w", err)
|
||||||
}
|
}
|
||||||
cli.NegotiateAPIVersion(ctx)
|
// Best-effort API version negotiation, matching the old client.NegotiateAPIVersion
|
||||||
|
// behaviour. Ping failures here are non-fatal: the connection is exercised again on
|
||||||
|
// the first real call, and the client falls back to the daemon's default API version.
|
||||||
|
if _, err := cli.Ping(ctx, client.PingOptions{NegotiateAPIVersion: true}); err != nil {
|
||||||
|
common.Logger(ctx).Warnf("docker daemon ping during version negotiation failed, continuing: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return cli, nil
|
return cli, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHostInfo(ctx context.Context) (info types.Info, err error) { //nolint:staticcheck // pre-existing issue from nektos/act
|
func GetHostInfo(ctx context.Context) (info system.Info, err error) {
|
||||||
var cli client.APIClient
|
var cli client.APIClient
|
||||||
cli, err = GetDockerClient(ctx)
|
cli, err = GetDockerClient(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -245,12 +257,12 @@ func GetHostInfo(ctx context.Context) (info types.Info, err error) { //nolint:st
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
info, err = cli.Info(ctx)
|
result, err := cli.Info(ctx, client.InfoOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return info, nil
|
return result.Info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arch fetches values from docker info and translates architecture to
|
// Arch fetches values from docker info and translates architecture to
|
||||||
@@ -307,14 +319,14 @@ func (cr *containerReference) find() common.Executor {
|
|||||||
if cr.id != "" {
|
if cr.id != "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
containers, err := cr.cli.ContainerList(ctx, types.ContainerListOptions{ //nolint:staticcheck // pre-existing issue from nektos/act
|
containers, err := cr.cli.ContainerList(ctx, client.ContainerListOptions{
|
||||||
All: true,
|
All: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to list containers: %w", err)
|
return fmt.Errorf("failed to list containers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range containers {
|
for _, c := range containers.Items {
|
||||||
for _, name := range c.Names {
|
for _, name := range c.Names {
|
||||||
if name[1:] == cr.input.Name {
|
if name[1:] == cr.input.Name {
|
||||||
cr.id = c.ID
|
cr.id = c.ID
|
||||||
@@ -335,7 +347,7 @@ func (cr *containerReference) remove() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
err := cr.cli.ContainerRemove(ctx, cr.id, types.ContainerRemoveOptions{ //nolint:staticcheck // pre-existing issue from nektos/act
|
_, err := cr.cli.ContainerRemove(ctx, cr.id, client.ContainerRemoveOptions{
|
||||||
RemoveVolumes: true,
|
RemoveVolumes: true,
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
})
|
||||||
@@ -438,15 +450,22 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
|
||||||
input := cr.input
|
input := cr.input
|
||||||
|
exposedPorts, err := convertPortSet(input.ExposedPorts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
portBindings, err := convertPortMap(input.PortBindings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
config := &container.Config{
|
config := &container.Config{
|
||||||
Image: input.Image,
|
Image: input.Image,
|
||||||
WorkingDir: input.WorkingDir,
|
WorkingDir: input.WorkingDir,
|
||||||
Env: input.Env,
|
Env: input.Env,
|
||||||
ExposedPorts: input.ExposedPorts,
|
ExposedPorts: exposedPorts,
|
||||||
Tty: isTerminal,
|
Tty: input.AllocatePTY,
|
||||||
}
|
}
|
||||||
// For Gitea, reduce log noise
|
// For Gitea, reduce log noise
|
||||||
// logger.Debugf("Common container.Config ==> %+v", config)
|
// logger.Debugf("Common container.Config ==> %+v", config)
|
||||||
@@ -470,15 +489,9 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
|||||||
|
|
||||||
var platSpecs *specs.Platform
|
var platSpecs *specs.Platform
|
||||||
if supportsContainerImagePlatform(ctx, cr.cli) && cr.input.Platform != "" {
|
if supportsContainerImagePlatform(ctx, cr.cli) && cr.input.Platform != "" {
|
||||||
desiredPlatform := strings.SplitN(cr.input.Platform, `/`, 2)
|
platSpecs, err = parsePlatform(cr.input.Platform)
|
||||||
|
if err != nil {
|
||||||
if len(desiredPlatform) != 2 {
|
return err
|
||||||
return fmt.Errorf("incorrect container platform option '%s'", cr.input.Platform)
|
|
||||||
}
|
|
||||||
|
|
||||||
platSpecs = &specs.Platform{
|
|
||||||
Architecture: desiredPlatform[1],
|
|
||||||
OS: desiredPlatform[0],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,13 +503,13 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
|||||||
NetworkMode: container.NetworkMode(input.NetworkMode),
|
NetworkMode: container.NetworkMode(input.NetworkMode),
|
||||||
Privileged: input.Privileged,
|
Privileged: input.Privileged,
|
||||||
UsernsMode: container.UsernsMode(input.UsernsMode),
|
UsernsMode: container.UsernsMode(input.UsernsMode),
|
||||||
PortBindings: input.PortBindings,
|
PortBindings: portBindings,
|
||||||
AutoRemove: input.AutoRemove,
|
AutoRemove: input.AutoRemove,
|
||||||
}
|
}
|
||||||
// For Gitea, reduce log noise
|
// For Gitea, reduce log noise
|
||||||
// logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
|
// logger.Debugf("Common container.HostConfig ==> %+v", hostConfig)
|
||||||
|
|
||||||
config, hostConfig, err := cr.mergeContainerConfigs(ctx, config, hostConfig)
|
config, hostConfig, err = cr.mergeContainerConfigs(ctx, config, hostConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -520,7 +533,13 @@ func (cr *containerReference) create(capAdd, capDrop []string) common.Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := cr.cli.ContainerCreate(ctx, config, hostConfig, networkingConfig, platSpecs, input.Name)
|
resp, err := cr.cli.ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||||
|
Config: config,
|
||||||
|
HostConfig: hostConfig,
|
||||||
|
NetworkingConfig: networkingConfig,
|
||||||
|
Platform: platSpecs,
|
||||||
|
Name: input.Name,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create container: '%w'", err)
|
return fmt.Errorf("failed to create container: '%w'", err)
|
||||||
}
|
}
|
||||||
@@ -538,7 +557,7 @@ func (cr *containerReference) extractFromImageEnv(env *map[string]string) common
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
inspect, _, err := cr.cli.ImageInspectWithRaw(ctx, cr.input.Image)
|
inspect, err := cr.cli.ImageInspect(ctx, cr.input.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
return fmt.Errorf("inspect image: %w", err)
|
return fmt.Errorf("inspect image: %w", err)
|
||||||
@@ -584,7 +603,7 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("Exec command '%s'", cmd)
|
logger.Debugf("Exec command '%s'", cmd)
|
||||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
isTerminal := cr.input.AllocatePTY
|
||||||
envList := make([]string, 0)
|
envList := make([]string, 0)
|
||||||
for k, v := range env {
|
for k, v := range env {
|
||||||
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
||||||
@@ -602,12 +621,12 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
|||||||
}
|
}
|
||||||
logger.Debugf("Working directory '%s'", wd)
|
logger.Debugf("Working directory '%s'", wd)
|
||||||
|
|
||||||
idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{
|
idResp, err := cr.cli.ExecCreate(ctx, cr.id, client.ExecCreateOptions{
|
||||||
User: user,
|
User: user,
|
||||||
Cmd: cmd,
|
Cmd: cmd,
|
||||||
WorkingDir: wd,
|
WorkingDir: wd,
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Tty: isTerminal,
|
TTY: isTerminal,
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
})
|
})
|
||||||
@@ -615,20 +634,20 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
|||||||
return fmt.Errorf("failed to create exec: %w", err)
|
return fmt.Errorf("failed to create exec: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{
|
resp, err := cr.cli.ExecAttach(ctx, idResp.ID, client.ExecAttachOptions{
|
||||||
Tty: isTerminal,
|
TTY: isTerminal,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to attach to exec: %w", err)
|
return fmt.Errorf("failed to attach to exec: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Close()
|
defer resp.Close()
|
||||||
|
|
||||||
err = cr.waitForCommand(ctx, isTerminal, resp, idResp, user, workdir)
|
err = cr.waitForCommand(ctx, isTerminal, resp.HijackedResponse, idResp, user, workdir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
inspectResp, err := cr.cli.ContainerExecInspect(ctx, idResp.ID)
|
inspectResp, err := cr.cli.ExecInspect(ctx, idResp.ID, client.ExecInspectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to inspect exec: %w", err)
|
return fmt.Errorf("failed to inspect exec: %w", err)
|
||||||
}
|
}
|
||||||
@@ -642,7 +661,7 @@ func (cr *containerReference) exec(cmd []string, env map[string]string, user, wo
|
|||||||
|
|
||||||
func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Executor {
|
func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
idResp, err := cr.cli.ContainerExecCreate(ctx, cr.id, types.ExecConfig{
|
idResp, err := cr.cli.ExecCreate(ctx, cr.id, client.ExecCreateOptions{
|
||||||
Cmd: []string{"id", opt},
|
Cmd: []string{"id", opt},
|
||||||
AttachStdout: true,
|
AttachStdout: true,
|
||||||
AttachStderr: true,
|
AttachStderr: true,
|
||||||
@@ -651,7 +670,7 @@ func (cr *containerReference) tryReadID(opt string, cbk func(id int)) common.Exe
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := cr.cli.ContainerExecAttach(ctx, idResp.ID, types.ExecStartCheck{})
|
resp, err := cr.cli.ExecAttach(ctx, idResp.ID, client.ExecAttachOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -681,7 +700,7 @@ func (cr *containerReference) tryReadGID() common.Executor {
|
|||||||
return cr.tryReadID("-g", func(id int) { cr.GID = id })
|
return cr.tryReadID("-g", func(id int) { cr.GID = id })
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp types.HijackedResponse, _ types.IDResponse, _, _ string) error {
|
func (cr *containerReference) waitForCommand(ctx context.Context, isTerminal bool, resp client.HijackedResponse, _ client.ExecCreateResult, _, _ string) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
cmdResponse := make(chan error)
|
cmdResponse := make(chan error)
|
||||||
@@ -736,12 +755,18 @@ func (cr *containerReference) CopyTarStream(ctx context.Context, destPath string
|
|||||||
Typeflag: tar.TypeDir,
|
Typeflag: tar.TypeDir,
|
||||||
})
|
})
|
||||||
tw.Close()
|
tw.Close()
|
||||||
err := cr.cli.CopyToContainer(ctx, cr.id, "/", buf, types.CopyToContainerOptions{})
|
_, err := cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
|
||||||
|
DestinationPath: "/",
|
||||||
|
Content: buf,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to mkdir to copy content to container: %w", err)
|
return fmt.Errorf("failed to mkdir to copy content to container: %w", err)
|
||||||
}
|
}
|
||||||
// Copy Content
|
// Copy Content
|
||||||
err = cr.cli.CopyToContainer(ctx, cr.id, destPath, tarStream, types.CopyToContainerOptions{})
|
_, err = cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
|
||||||
|
DestinationPath: destPath,
|
||||||
|
Content: tarStream,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to copy content to container: %w", err)
|
return fmt.Errorf("failed to copy content to container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -815,7 +840,10 @@ func (cr *containerReference) copyDir(dstPath, srcPath string, useGitIgnore bool
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to seek tar archive: %w", err)
|
return fmt.Errorf("failed to seek tar archive: %w", err)
|
||||||
}
|
}
|
||||||
err = cr.cli.CopyToContainer(ctx, cr.id, "/", tarFile, types.CopyToContainerOptions{})
|
_, err = cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
|
||||||
|
DestinationPath: "/",
|
||||||
|
Content: tarFile,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to copy content to container: %w", err)
|
return fmt.Errorf("failed to copy content to container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -849,7 +877,10 @@ func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) c
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Debugf("Extracting content to '%s'", dstPath)
|
logger.Debugf("Extracting content to '%s'", dstPath)
|
||||||
err := cr.cli.CopyToContainer(ctx, cr.id, dstPath, &buf, types.CopyToContainerOptions{})
|
_, err := cr.cli.CopyToContainer(ctx, cr.id, client.CopyToContainerOptions{
|
||||||
|
DestinationPath: dstPath,
|
||||||
|
Content: &buf,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to copy content to container: %w", err)
|
return fmt.Errorf("failed to copy content to container: %w", err)
|
||||||
}
|
}
|
||||||
@@ -859,7 +890,7 @@ func (cr *containerReference) copyContent(dstPath string, files ...*FileEntry) c
|
|||||||
|
|
||||||
func (cr *containerReference) attach() common.Executor {
|
func (cr *containerReference) attach() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
out, err := cr.cli.ContainerAttach(ctx, cr.id, types.ContainerAttachOptions{ //nolint:staticcheck // pre-existing issue from nektos/act
|
out, err := cr.cli.ContainerAttach(ctx, cr.id, client.ContainerAttachOptions{
|
||||||
Stream: true,
|
Stream: true,
|
||||||
Stdout: true,
|
Stdout: true,
|
||||||
Stderr: true,
|
Stderr: true,
|
||||||
@@ -867,7 +898,7 @@ func (cr *containerReference) attach() common.Executor {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to attach to container: %w", err)
|
return fmt.Errorf("failed to attach to container: %w", err)
|
||||||
}
|
}
|
||||||
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
|
isTerminal := cr.input.AllocatePTY
|
||||||
|
|
||||||
var outWriter io.Writer
|
var outWriter io.Writer
|
||||||
outWriter = cr.input.Stdout
|
outWriter = cr.input.Stdout
|
||||||
@@ -897,7 +928,7 @@ func (cr *containerReference) start() common.Executor {
|
|||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Debugf("Starting container: %v", cr.id)
|
logger.Debugf("Starting container: %v", cr.id)
|
||||||
|
|
||||||
if err := cr.cli.ContainerStart(ctx, cr.id, types.ContainerStartOptions{}); err != nil { //nolint:staticcheck // pre-existing issue from nektos/act
|
if _, err := cr.cli.ContainerStart(ctx, cr.id, client.ContainerStartOptions{}); err != nil {
|
||||||
return fmt.Errorf("failed to start container: %w", err)
|
return fmt.Errorf("failed to start container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -909,14 +940,16 @@ func (cr *containerReference) start() common.Executor {
|
|||||||
func (cr *containerReference) wait() common.Executor {
|
func (cr *containerReference) wait() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
statusCh, errCh := cr.cli.ContainerWait(ctx, cr.id, container.WaitConditionNotRunning)
|
waitResult := cr.cli.ContainerWait(ctx, cr.id, client.ContainerWaitOptions{
|
||||||
|
Condition: container.WaitConditionNotRunning,
|
||||||
|
})
|
||||||
var statusCode int64
|
var statusCode int64
|
||||||
select {
|
select {
|
||||||
case err := <-errCh:
|
case err := <-waitResult.Error:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to wait for container: %w", err)
|
return fmt.Errorf("failed to wait for container: %w", err)
|
||||||
}
|
}
|
||||||
case status := <-statusCh:
|
case status := <-waitResult.Result:
|
||||||
statusCode = status.StatusCode
|
statusCode = status.StatusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,22 +969,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
|
||||||
if len(cr.input.ValidVolumes) > 0 {
|
if len(cr.input.ValidVolumes) > 0 {
|
||||||
globs := make([]glob.Glob, 0, len(cr.input.ValidVolumes))
|
matcher := newValidVolumeMatcher(ctx, cr.input.ValidVolumes)
|
||||||
for _, v := range cr.input.ValidVolumes {
|
|
||||||
if g, err := glob.Compile(v); err != nil {
|
|
||||||
logger.Errorf("create glob from %s error: %v", v, err)
|
|
||||||
} else {
|
|
||||||
globs = append(globs, g)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isValid := func(v string) bool {
|
|
||||||
for _, g := range globs {
|
|
||||||
if g.Match(v) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// sanitize binds
|
// sanitize binds
|
||||||
sanitizedBinds := make([]string, 0, len(hostConfig.Binds))
|
sanitizedBinds := make([]string, 0, len(hostConfig.Binds))
|
||||||
for _, bind := range hostConfig.Binds {
|
for _, bind := range hostConfig.Binds {
|
||||||
@@ -965,7 +983,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
sanitizedBinds = append(sanitizedBinds, bind)
|
sanitizedBinds = append(sanitizedBinds, bind)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isValid(parsed.Source) {
|
if matcher.isValid(parsed.Source, mount.Type(parsed.Type)) {
|
||||||
sanitizedBinds = append(sanitizedBinds, bind)
|
sanitizedBinds = append(sanitizedBinds, bind)
|
||||||
} else {
|
} else {
|
||||||
logger.Warnf("[%s] is not a valid volume, will be ignored", parsed.Source)
|
logger.Warnf("[%s] is not a valid volume, will be ignored", parsed.Source)
|
||||||
@@ -975,7 +993,7 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
// sanitize mounts
|
// sanitize mounts
|
||||||
sanitizedMounts := make([]mount.Mount, 0, len(hostConfig.Mounts))
|
sanitizedMounts := make([]mount.Mount, 0, len(hostConfig.Mounts))
|
||||||
for _, mt := range hostConfig.Mounts {
|
for _, mt := range hostConfig.Mounts {
|
||||||
if isValid(mt.Source) {
|
if matcher.isValid(mt.Source, mt.Type) {
|
||||||
sanitizedMounts = append(sanitizedMounts, mt)
|
sanitizedMounts = append(sanitizedMounts, mt)
|
||||||
} else {
|
} else {
|
||||||
logger.Warnf("[%s] is not a valid volume, will be ignored", mt.Source)
|
logger.Warnf("[%s] is not a valid volume, will be ignored", mt.Source)
|
||||||
@@ -989,3 +1007,129 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
|
|
||||||
return config, hostConfig
|
return config, hostConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type validVolumeMatcher struct {
|
||||||
|
allowAll bool
|
||||||
|
named []glob.Glob
|
||||||
|
host []glob.Glob
|
||||||
|
}
|
||||||
|
|
||||||
|
func newValidVolumeMatcher(ctx context.Context, validVolumes []string) validVolumeMatcher {
|
||||||
|
logger := common.Logger(ctx)
|
||||||
|
ret := validVolumeMatcher{
|
||||||
|
named: make([]glob.Glob, 0, len(validVolumes)),
|
||||||
|
host: make([]glob.Glob, 0, len(validVolumes)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range validVolumes {
|
||||||
|
if v == "**" {
|
||||||
|
ret.allowAll = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isHostVolumePattern(v) {
|
||||||
|
if g, err := glob.Compile(v); err != nil {
|
||||||
|
logger.Errorf("create glob from %s error: %v", v, err)
|
||||||
|
} else {
|
||||||
|
ret.named = append(ret.named, g)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
normalized, err := normalizeHostVolumePath(v)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("normalize volume pattern %s error: %v", v, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if g, err := glob.Compile(normalized); err != nil {
|
||||||
|
logger.Errorf("create glob from %s error: %v", normalized, err)
|
||||||
|
} else {
|
||||||
|
ret.host = append(ret.host, g)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m validVolumeMatcher) isValid(source string, sourceType mount.Type) bool {
|
||||||
|
if m.allowAll {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isHostVolumeSource(source, sourceType) {
|
||||||
|
normalized, err := normalizeHostVolumePath(source)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, g := range m.host {
|
||||||
|
if g.Match(normalized) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, g := range m.named {
|
||||||
|
if g.Match(source) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHostVolumePattern(pattern string) bool {
|
||||||
|
return filepath.IsAbs(pattern) ||
|
||||||
|
strings.HasPrefix(pattern, "."+string(filepath.Separator)) ||
|
||||||
|
strings.HasPrefix(pattern, ".."+string(filepath.Separator)) ||
|
||||||
|
strings.Contains(pattern, "/") ||
|
||||||
|
strings.Contains(pattern, `\`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHostVolumeSource(source string, sourceType mount.Type) bool {
|
||||||
|
if sourceType == mount.TypeBind {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if sourceType == mount.TypeVolume {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return isHostVolumePattern(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeHostVolumePath(path string) (string, error) {
|
||||||
|
abs, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return evalSymlinksExistingPrefix(abs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func evalSymlinksExistingPrefix(path string) (string, error) {
|
||||||
|
resolved, err := filepath.EvalSymlinks(path)
|
||||||
|
if err == nil {
|
||||||
|
return filepath.Clean(resolved), nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
current := path
|
||||||
|
var missing []string
|
||||||
|
for {
|
||||||
|
_, err := os.Lstat(current)
|
||||||
|
if err == nil {
|
||||||
|
resolved, err := filepath.EvalSymlinks(current)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, name := range slices.Backward(missing) {
|
||||||
|
resolved = filepath.Join(resolved, name)
|
||||||
|
}
|
||||||
|
return filepath.Clean(resolved), nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(current)
|
||||||
|
if parent == current {
|
||||||
|
return filepath.Clean(path), nil
|
||||||
|
}
|
||||||
|
missing = append(missing, filepath.Base(current))
|
||||||
|
current = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/moby/moby/api/types/container"
|
||||||
"github.com/docker/docker/api/types/container"
|
mobyclient "github.com/moby/moby/client"
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/sirupsen/logrus/hooks/test"
|
"github.com/sirupsen/logrus/hooks/test"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
@@ -27,9 +28,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDocker(t *testing.T) {
|
func TestDocker(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client, err := GetDockerClient(ctx)
|
client, err := GetDockerClient(ctx)
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
if err != nil {
|
||||||
|
t.Skipf("skipping integration test: %v", err)
|
||||||
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{
|
dockerBuild := NewDockerBuildExecutor(NewDockerBuildExecutorInput{
|
||||||
@@ -67,33 +73,33 @@ func TestDocker(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mockDockerClient struct {
|
type mockDockerClient struct {
|
||||||
client.APIClient
|
mobyclient.APIClient
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) ContainerExecCreate(ctx context.Context, id string, opts types.ExecConfig) (types.IDResponse, error) {
|
func (m *mockDockerClient) ExecCreate(ctx context.Context, id string, opts mobyclient.ExecCreateOptions) (mobyclient.ExecCreateResult, error) {
|
||||||
args := m.Called(ctx, id, opts)
|
args := m.Called(ctx, id, opts)
|
||||||
return args.Get(0).(types.IDResponse), args.Error(1)
|
return args.Get(0).(mobyclient.ExecCreateResult), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) ContainerExecAttach(ctx context.Context, id string, opts types.ExecStartCheck) (types.HijackedResponse, error) {
|
func (m *mockDockerClient) ExecAttach(ctx context.Context, id string, opts mobyclient.ExecAttachOptions) (mobyclient.ExecAttachResult, error) {
|
||||||
args := m.Called(ctx, id, opts)
|
args := m.Called(ctx, id, opts)
|
||||||
return args.Get(0).(types.HijackedResponse), args.Error(1)
|
return args.Get(0).(mobyclient.ExecAttachResult), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) ContainerExecInspect(ctx context.Context, execID string) (types.ContainerExecInspect, error) {
|
func (m *mockDockerClient) ExecInspect(ctx context.Context, execID string, opts mobyclient.ExecInspectOptions) (mobyclient.ExecInspectResult, error) {
|
||||||
args := m.Called(ctx, execID)
|
args := m.Called(ctx, execID, opts)
|
||||||
return args.Get(0).(types.ContainerExecInspect), args.Error(1)
|
return args.Get(0).(mobyclient.ExecInspectResult), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
func (m *mockDockerClient) ContainerWait(ctx context.Context, containerID string, opts mobyclient.ContainerWaitOptions) mobyclient.ContainerWaitResult {
|
||||||
args := m.Called(ctx, containerID, condition)
|
args := m.Called(ctx, containerID, opts)
|
||||||
return args.Get(0).(<-chan container.WaitResponse), args.Get(1).(<-chan error)
|
return args.Get(0).(mobyclient.ContainerWaitResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id, path string, content io.Reader, options types.CopyToContainerOptions) error {
|
func (m *mockDockerClient) CopyToContainer(ctx context.Context, id string, options mobyclient.CopyToContainerOptions) (mobyclient.CopyToContainerResult, error) {
|
||||||
args := m.Called(ctx, id, path, content, options)
|
args := m.Called(ctx, id, options)
|
||||||
return args.Error(0)
|
return args.Get(0).(mobyclient.CopyToContainerResult), args.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
type endlessReader struct {
|
type endlessReader struct {
|
||||||
@@ -125,10 +131,12 @@ func TestDockerExecAbort(t *testing.T) {
|
|||||||
conn.On("Write", mock.AnythingOfType("[]uint8")).Return(1, nil)
|
conn.On("Write", mock.AnythingOfType("[]uint8")).Return(1, nil)
|
||||||
|
|
||||||
client := &mockDockerClient{}
|
client := &mockDockerClient{}
|
||||||
client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil)
|
client.On("ExecCreate", ctx, "123", mock.AnythingOfType("client.ExecCreateOptions")).Return(mobyclient.ExecCreateResult{ID: "id"}, nil)
|
||||||
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{
|
client.On("ExecAttach", ctx, "id", mock.AnythingOfType("client.ExecAttachOptions")).Return(mobyclient.ExecAttachResult{
|
||||||
|
HijackedResponse: mobyclient.HijackedResponse{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
Reader: bufio.NewReader(endlessReader{}),
|
Reader: bufio.NewReader(endlessReader{}),
|
||||||
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
@@ -162,12 +170,14 @@ func TestDockerExecFailure(t *testing.T) {
|
|||||||
conn := &mockConn{}
|
conn := &mockConn{}
|
||||||
|
|
||||||
client := &mockDockerClient{}
|
client := &mockDockerClient{}
|
||||||
client.On("ContainerExecCreate", ctx, "123", mock.AnythingOfType("types.ExecConfig")).Return(types.IDResponse{ID: "id"}, nil)
|
client.On("ExecCreate", ctx, "123", mock.AnythingOfType("client.ExecCreateOptions")).Return(mobyclient.ExecCreateResult{ID: "id"}, nil)
|
||||||
client.On("ContainerExecAttach", ctx, "id", mock.AnythingOfType("types.ExecStartCheck")).Return(types.HijackedResponse{
|
client.On("ExecAttach", ctx, "id", mock.AnythingOfType("client.ExecAttachOptions")).Return(mobyclient.ExecAttachResult{
|
||||||
|
HijackedResponse: mobyclient.HijackedResponse{
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
Reader: bufio.NewReader(strings.NewReader("output")),
|
Reader: bufio.NewReader(strings.NewReader("output")),
|
||||||
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
client.On("ContainerExecInspect", ctx, "id").Return(types.ContainerExecInspect{
|
client.On("ExecInspect", ctx, "id", mobyclient.ExecInspectOptions{}).Return(mobyclient.ExecInspectResult{
|
||||||
ExitCode: 1,
|
ExitCode: 1,
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
@@ -197,8 +207,11 @@ func TestDockerWaitFailure(t *testing.T) {
|
|||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
|
|
||||||
client := &mockDockerClient{}
|
client := &mockDockerClient{}
|
||||||
client.On("ContainerWait", ctx, "123", container.WaitConditionNotRunning).
|
client.On("ContainerWait", ctx, "123", mobyclient.ContainerWaitOptions{Condition: container.WaitConditionNotRunning}).
|
||||||
Return((<-chan container.WaitResponse)(statusCh), (<-chan error)(errCh))
|
Return(mobyclient.ContainerWaitResult{
|
||||||
|
Result: (<-chan container.WaitResponse)(statusCh),
|
||||||
|
Error: (<-chan error)(errCh),
|
||||||
|
})
|
||||||
|
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
id: "123",
|
id: "123",
|
||||||
@@ -220,11 +233,13 @@ func TestDockerWaitFailure(t *testing.T) {
|
|||||||
func TestDockerCopyTarStream(t *testing.T) {
|
func TestDockerCopyTarStream(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
conn := &mockConn{}
|
|
||||||
|
|
||||||
client := &mockDockerClient{}
|
client := &mockDockerClient{}
|
||||||
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil)
|
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
|
||||||
client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil)
|
return opts.DestinationPath == "/" && opts.Content != nil
|
||||||
|
})).Return(mobyclient.CopyToContainerResult{}, nil)
|
||||||
|
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
|
||||||
|
return opts.DestinationPath == "/var/run/act" && opts.Content != nil
|
||||||
|
})).Return(mobyclient.CopyToContainerResult{}, nil)
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
id: "123",
|
id: "123",
|
||||||
cli: client,
|
cli: client,
|
||||||
@@ -235,20 +250,18 @@ func TestDockerCopyTarStream(t *testing.T) {
|
|||||||
|
|
||||||
_ = cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
|
_ = cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
|
||||||
|
|
||||||
conn.AssertExpectations(t)
|
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) {
|
func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
conn := &mockConn{}
|
|
||||||
|
|
||||||
merr := errors.New("Failure")
|
merr := errors.New("Failure")
|
||||||
|
|
||||||
client := &mockDockerClient{}
|
client := &mockDockerClient{}
|
||||||
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr)
|
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
|
||||||
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr)
|
return opts.DestinationPath == "/" && opts.Content != nil
|
||||||
|
})).Return(mobyclient.CopyToContainerResult{}, merr)
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
id: "123",
|
id: "123",
|
||||||
cli: client,
|
cli: client,
|
||||||
@@ -260,20 +273,21 @@ func TestDockerCopyTarStreamErrorInCopyFiles(t *testing.T) {
|
|||||||
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
|
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
|
||||||
assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
conn.AssertExpectations(t)
|
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
|
func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
conn := &mockConn{}
|
|
||||||
|
|
||||||
merr := errors.New("Failure")
|
merr := errors.New("Failure")
|
||||||
|
|
||||||
client := &mockDockerClient{}
|
client := &mockDockerClient{}
|
||||||
client.On("CopyToContainer", ctx, "123", "/", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(nil)
|
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
|
||||||
client.On("CopyToContainer", ctx, "123", "/var/run/act", mock.Anything, mock.AnythingOfType("types.CopyToContainerOptions")).Return(merr)
|
return opts.DestinationPath == "/" && opts.Content != nil
|
||||||
|
})).Return(mobyclient.CopyToContainerResult{}, nil)
|
||||||
|
client.On("CopyToContainer", ctx, "123", mock.MatchedBy(func(opts mobyclient.CopyToContainerOptions) bool {
|
||||||
|
return opts.DestinationPath == "/var/run/act" && opts.Content != nil
|
||||||
|
})).Return(mobyclient.CopyToContainerResult{}, merr)
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
id: "123",
|
id: "123",
|
||||||
cli: client,
|
cli: client,
|
||||||
@@ -285,7 +299,6 @@ func TestDockerCopyTarStreamErrorInMkdir(t *testing.T) {
|
|||||||
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
|
err := cr.CopyTarStream(ctx, "/var/run/act", &bytes.Buffer{})
|
||||||
assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
|
assert.ErrorIs(t, err, merr) //nolint:testifylint // pre-existing issue from nektos/act
|
||||||
|
|
||||||
conn.AssertExpectations(t)
|
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,3 +377,40 @@ func TestCheckVolumes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckVolumesRejectsEscapingHostPaths(t *testing.T) {
|
||||||
|
logger, _ := test.NewNullLogger()
|
||||||
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
|
|
||||||
|
base := t.TempDir()
|
||||||
|
allowed := filepath.Join(base, "allowed")
|
||||||
|
denied := filepath.Join(base, "denied")
|
||||||
|
require.NoError(t, os.MkdirAll(allowed, 0o700))
|
||||||
|
require.NoError(t, os.MkdirAll(denied, 0o700))
|
||||||
|
|
||||||
|
cr := &containerReference{
|
||||||
|
input: &NewContainerInput{
|
||||||
|
ValidVolumes: []string{filepath.Join(allowed, "**")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
escapingPath := allowed + string(filepath.Separator) + ".." + string(filepath.Separator) + "denied"
|
||||||
|
_, hostConf := cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
|
||||||
|
Binds: []string{escapingPath + ":/mnt"},
|
||||||
|
})
|
||||||
|
assert.Empty(t, hostConf.Binds)
|
||||||
|
|
||||||
|
linkPath := filepath.Join(allowed, "link")
|
||||||
|
if err := os.Symlink(denied, linkPath); err != nil {
|
||||||
|
t.Skipf("cannot create symlink: %v", err)
|
||||||
|
}
|
||||||
|
_, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
|
||||||
|
Binds: []string{linkPath + ":/mnt"},
|
||||||
|
})
|
||||||
|
assert.Empty(t, hostConf.Binds)
|
||||||
|
|
||||||
|
_, hostConf = cr.sanitizeConfig(ctx, &container.Config{}, &container.HostConfig{
|
||||||
|
Binds: []string{filepath.Join(linkPath, "missing") + ":/mnt"},
|
||||||
|
})
|
||||||
|
assert.Empty(t, hostConf.Binds)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/moby/moby/api/types/system"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ func RunnerArch(ctx context.Context) string {
|
|||||||
return runtime.GOOS
|
return runtime.GOOS
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetHostInfo(ctx context.Context) (info types.Info, err error) {
|
func GetHostInfo(ctx context.Context) (info system.Info, err error) {
|
||||||
return types.Info{}, nil
|
return system.Info{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor {
|
func NewDockerVolumeRemoveExecutor(volume string, force bool) common.Executor {
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import (
|
|||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/moby/moby/client"
|
||||||
"github.com/docker/docker/api/types/volume"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executor {
|
func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executor {
|
||||||
@@ -23,12 +22,12 @@ func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executo
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
list, err := cli.VolumeList(ctx, volume.ListOptions{Filters: filters.NewArgs()})
|
list, err := cli.VolumeList(ctx, client.VolumeListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, vol := range list.Volumes {
|
for _, vol := range list.Items {
|
||||||
if vol.Name == volumeName {
|
if vol.Name == volumeName {
|
||||||
return removeExecutor(volumeName, force)(ctx)
|
return removeExecutor(volumeName, force)(ctx)
|
||||||
}
|
}
|
||||||
@@ -42,7 +41,7 @@ func NewDockerVolumeRemoveExecutor(volumeName string, force bool) common.Executo
|
|||||||
func removeExecutor(volume string, force bool) common.Executor {
|
func removeExecutor(volume string, force bool) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
logger.Debugf("%sdocker volume rm %s", logPrefix, volume)
|
logger.Debugf("docker volume rm %s", volume)
|
||||||
|
|
||||||
if common.Dryrun(ctx) {
|
if common.Dryrun(ctx) {
|
||||||
return nil
|
return nil
|
||||||
@@ -54,6 +53,7 @@ func removeExecutor(volume string, force bool) common.Executor {
|
|||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
return cli.VolumeRemove(ctx, volume, force)
|
_, err = cli.VolumeRemove(ctx, volume, client.VolumeRemoveOptions{Force: force})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -36,12 +37,13 @@ type HostEnvironment struct {
|
|||||||
TmpDir string
|
TmpDir string
|
||||||
ToolCache string
|
ToolCache string
|
||||||
Workdir string
|
Workdir string
|
||||||
// BindWorkdir is true when the app runner mounts the workspace on the host and
|
// CleanWorkdir means teardown owns Workdir and may delete it. Leave false
|
||||||
// deletes the task directory after the job; host teardown must not remove Workdir.
|
// when Workdir points at a caller-owned checkout (e.g. `act` local mode).
|
||||||
BindWorkdir bool
|
CleanWorkdir bool
|
||||||
ActPath string
|
ActPath string
|
||||||
CleanUp func()
|
CleanUp func()
|
||||||
StdOut io.Writer
|
StdOut io.Writer
|
||||||
|
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
runningPIDs map[int]struct{}
|
runningPIDs map[int]struct{}
|
||||||
@@ -200,12 +202,12 @@ func (e *HostEnvironment) Start(_ bool) common.Executor {
|
|||||||
|
|
||||||
type ptyWriter struct {
|
type ptyWriter struct {
|
||||||
Out io.Writer
|
Out io.Writer
|
||||||
AutoStop bool
|
AutoStop atomic.Bool
|
||||||
dirtyLine bool
|
dirtyLine bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *ptyWriter) Write(buf []byte) (int, error) {
|
func (w *ptyWriter) Write(buf []byte) (int, error) {
|
||||||
if w.AutoStop && len(buf) > 0 && buf[len(buf)-1] == 4 {
|
if w.AutoStop.Load() && len(buf) > 0 && buf[len(buf)-1] == 4 {
|
||||||
n, err := w.Out.Write(buf[:len(buf)-1])
|
n, err := w.Out.Write(buf[:len(buf)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
@@ -335,21 +337,20 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
|||||||
tty.Close()
|
tty.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if true /* allocate Terminal */ {
|
if e.AllocatePTY {
|
||||||
var err error
|
var err error
|
||||||
ppty, tty, err = setupPty(cmd, cmdline)
|
ppty, tty, err = setupPty(cmd, cmdline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
|
common.Logger(ctx).Debugf("Failed to setup Pty %v\n", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writer := &ptyWriter{Out: e.StdOut}
|
var writer *ptyWriter
|
||||||
logctx, finishLog := context.WithCancel(context.Background())
|
var logctx context.Context
|
||||||
if ppty != nil {
|
if ppty != nil {
|
||||||
|
writer = &ptyWriter{Out: e.StdOut}
|
||||||
|
var finishLog context.CancelFunc
|
||||||
|
logctx, finishLog = context.WithCancel(context.Background())
|
||||||
go copyPtyOutput(writer, ppty, finishLog)
|
go copyPtyOutput(writer, ppty, finishLog)
|
||||||
} else {
|
|
||||||
finishLog()
|
|
||||||
}
|
|
||||||
if ppty != nil {
|
|
||||||
go writeKeepAlive(ppty)
|
go writeKeepAlive(ppty)
|
||||||
}
|
}
|
||||||
// Split Start/Wait so the PID can be registered before the process can exit;
|
// Split Start/Wait so the PID can be registered before the process can exit;
|
||||||
@@ -379,14 +380,11 @@ func (e *HostEnvironment) exec(ctx context.Context, command []string, cmdline st
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tty != nil {
|
if tty != nil {
|
||||||
writer.AutoStop = true
|
writer.AutoStop.Store(true)
|
||||||
if _, err := tty.WriteString("\x04"); err != nil {
|
if _, err := tty.WriteString("\x04"); err != nil {
|
||||||
common.Logger(ctx).Debug("Failed to write EOT")
|
common.Logger(ctx).Debug("Failed to write EOT")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
<-logctx.Done()
|
<-logctx.Done()
|
||||||
|
|
||||||
if ppty != nil {
|
|
||||||
ppty.Close()
|
ppty.Close()
|
||||||
ppty = nil
|
ppty = nil
|
||||||
}
|
}
|
||||||
@@ -485,7 +483,7 @@ func (e *HostEnvironment) Remove() common.Executor {
|
|||||||
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
|
logger.Warnf("failed to remove host misc state %s: %v", e.Path, err)
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
if !e.BindWorkdir && e.Workdir != "" {
|
if e.CleanWorkdir {
|
||||||
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
|
if err := removePathWithRetry(ctx, e.Workdir); err != nil {
|
||||||
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
|
logger.Warnf("failed to remove host workspace %s: %v", e.Workdir, err)
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
@@ -100,7 +102,46 @@ func TestHostEnvironmentExecExitCode(t *testing.T) {
|
|||||||
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
assert.Equal(t, "Process completed with exit code 3.", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
func TestHostEnvironmentAllocatePTY(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("uses POSIX shell")
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
allocPTY bool
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{name: "off", allocPTY: false, expect: "NOTTY"},
|
||||||
|
{name: "on", allocPTY: true, expect: "TTY"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
e := &HostEnvironment{
|
||||||
|
Path: filepath.Join(dir, "path"),
|
||||||
|
TmpDir: filepath.Join(dir, "tmp"),
|
||||||
|
ToolCache: filepath.Join(dir, "tool_cache"),
|
||||||
|
ActPath: filepath.Join(dir, "act_path"),
|
||||||
|
StdOut: buf,
|
||||||
|
Workdir: filepath.Join(dir, "path"),
|
||||||
|
AllocatePTY: tc.allocPTY,
|
||||||
|
}
|
||||||
|
for _, p := range []string{e.Path, e.TmpDir, e.ToolCache, e.ActPath} {
|
||||||
|
require.NoError(t, os.MkdirAll(p, 0o700))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := e.Exec(
|
||||||
|
[]string{"sh", "-c", "[ -t 1 ] && printf TTY || printf NOTTY"},
|
||||||
|
map[string]string{"PATH": os.Getenv("PATH")}, "", "",
|
||||||
|
)(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
got := strings.TrimSpace(strings.ReplaceAll(buf.String(), "\r", ""))
|
||||||
|
assert.Equal(t, tc.expect, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEnvironmentRemovePreservesWorkdirByDefault(t *testing.T) {
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
@@ -113,7 +154,6 @@ func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
|||||||
e := &HostEnvironment{
|
e := &HostEnvironment{
|
||||||
Path: path,
|
Path: path,
|
||||||
Workdir: workdir,
|
Workdir: workdir,
|
||||||
BindWorkdir: false,
|
|
||||||
CleanUp: func() {
|
CleanUp: func() {
|
||||||
_ = os.RemoveAll(miscRoot)
|
_ = os.RemoveAll(miscRoot)
|
||||||
},
|
},
|
||||||
@@ -121,10 +161,10 @@ func TestHostEnvironmentRemoveCleansWorkdir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, e.Remove()(ctx))
|
require.NoError(t, e.Remove()(ctx))
|
||||||
_, err := os.Stat(workdir)
|
_, err := os.Stat(workdir)
|
||||||
assert.ErrorIs(t, err, os.ErrNotExist)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
func TestHostEnvironmentRemoveCleansWorkdirWhenOwned(t *testing.T) {
|
||||||
logger := logrus.New()
|
logger := logrus.New()
|
||||||
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
ctx := common.WithLogger(context.Background(), logrus.NewEntry(logger))
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
@@ -137,7 +177,7 @@ func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
|||||||
e := &HostEnvironment{
|
e := &HostEnvironment{
|
||||||
Path: path,
|
Path: path,
|
||||||
Workdir: workdir,
|
Workdir: workdir,
|
||||||
BindWorkdir: true,
|
CleanWorkdir: true,
|
||||||
CleanUp: func() {
|
CleanUp: func() {
|
||||||
_ = os.RemoveAll(miscRoot)
|
_ = os.RemoveAll(miscRoot)
|
||||||
},
|
},
|
||||||
@@ -145,5 +185,5 @@ func TestHostEnvironmentRemoveSkipsWorkdirWhenBindWorkdir(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, e.Remove()(ctx))
|
require.NoError(t, e.Remove()(ctx))
|
||||||
_, err := os.Stat(workdir)
|
_, err := os.Stat(workdir)
|
||||||
require.NoError(t, err)
|
assert.ErrorIs(t, err, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s := bufio.NewScanner(reader)
|
s := bufio.NewScanner(reader)
|
||||||
|
// Default 64 KiB max token size is too small for realistic env-file lines; allow up to 16 MiB.
|
||||||
|
s.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
||||||
for s.Scan() {
|
for s.Scan() {
|
||||||
line := s.Text()
|
line := s.Text()
|
||||||
singleLineEnv := strings.Index(line, "=")
|
singleLineEnv := strings.Index(line, "=")
|
||||||
@@ -50,6 +52,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
|||||||
}
|
}
|
||||||
multiLineEnvContent += content
|
multiLineEnvContent += content
|
||||||
}
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return fmt.Errorf("reading env file: %w", err)
|
||||||
|
}
|
||||||
if !delimiterFound {
|
if !delimiterFound {
|
||||||
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
|
return fmt.Errorf("invalid format delimiter '%v' not found before end of file", multiLineEnvDelimiter)
|
||||||
}
|
}
|
||||||
@@ -58,6 +63,9 @@ func parseEnvFile(e Container, srcPath string, env *map[string]string) common.Ex
|
|||||||
return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
|
return fmt.Errorf("invalid format '%v', expected a line with '=' or '<<'", line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := s.Err(); err != nil {
|
||||||
|
return fmt.Errorf("reading env file: %w", err)
|
||||||
|
}
|
||||||
env = &localEnv
|
env = &localEnv
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
75
act/container/parse_env_file_test.go
Normal file
75
act/container/parse_env_file_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestHostEnv(t *testing.T) (*HostEnvironment, string) {
|
||||||
|
t.Helper()
|
||||||
|
e := &HostEnvironment{Path: t.TempDir()}
|
||||||
|
return e, filepath.Join(e.Path, "envfile")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEnvFileSingleLine(t *testing.T) {
|
||||||
|
e, envPath := newTestHostEnv(t)
|
||||||
|
require.NoError(t, os.WriteFile(envPath, []byte("FOO=bar\nBAZ=qux\n"), 0o600))
|
||||||
|
|
||||||
|
env := map[string]string{}
|
||||||
|
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||||
|
assert.Equal(t, "bar", env["FOO"])
|
||||||
|
assert.Equal(t, "qux", env["BAZ"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEnvFileMultiLine(t *testing.T) {
|
||||||
|
e, envPath := newTestHostEnv(t)
|
||||||
|
content := "FOO<<EOF\nline1\nline2\nEOF\n"
|
||||||
|
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||||
|
|
||||||
|
env := map[string]string{}
|
||||||
|
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||||
|
assert.Equal(t, "line1\nline2", env["FOO"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEnvFileLargeValueWithinLimit(t *testing.T) {
|
||||||
|
e, envPath := newTestHostEnv(t)
|
||||||
|
big := strings.Repeat("x", 2*1024*1024)
|
||||||
|
content := "FOO<<EOF\n" + big + "\nEOF\n"
|
||||||
|
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||||
|
|
||||||
|
env := map[string]string{}
|
||||||
|
require.NoError(t, parseEnvFile(e, envPath, &env)(context.Background()))
|
||||||
|
assert.Equal(t, big, env["FOO"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEnvFileLineExceedsBufferReportsScannerError(t *testing.T) {
|
||||||
|
e, envPath := newTestHostEnv(t)
|
||||||
|
tooBig := strings.Repeat("x", 17*1024*1024) // over the 16 MiB cap
|
||||||
|
content := "FOO<<EOF\n" + tooBig + "\nEOF\n"
|
||||||
|
require.NoError(t, os.WriteFile(envPath, []byte(content), 0o600))
|
||||||
|
|
||||||
|
env := map[string]string{}
|
||||||
|
err := parseEnvFile(e, envPath, &env)(context.Background())
|
||||||
|
require.ErrorIs(t, err, bufio.ErrTooLong)
|
||||||
|
assert.Contains(t, err.Error(), "reading env file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEnvFileMissingDelimiter(t *testing.T) {
|
||||||
|
e, envPath := newTestHostEnv(t)
|
||||||
|
require.NoError(t, os.WriteFile(envPath, []byte("FOO<<EOF\nline1\nline2\n"), 0o600))
|
||||||
|
|
||||||
|
env := map[string]string{}
|
||||||
|
err := parseEnvFile(e, envPath, &env)(context.Background())
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "delimiter")
|
||||||
|
}
|
||||||
@@ -251,7 +251,7 @@ func (impl *interperterImpl) evaluateArrayDeref(arrayDerefNode *actionlint.Array
|
|||||||
|
|
||||||
func (impl *interperterImpl) getPropertyValue(left reflect.Value, property string) (value any, err error) {
|
func (impl *interperterImpl) getPropertyValue(left reflect.Value, property string) (value any, err error) {
|
||||||
switch left.Kind() {
|
switch left.Kind() {
|
||||||
case reflect.Ptr:
|
case reflect.Pointer:
|
||||||
return impl.getPropertyValue(left.Elem(), property)
|
return impl.getPropertyValue(left.Elem(), property)
|
||||||
|
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
@@ -321,7 +321,7 @@ func (impl *interperterImpl) getPropertyValue(left reflect.Value, property strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (impl *interperterImpl) getMapValue(value reflect.Value) (any, error) {
|
func (impl *interperterImpl) getMapValue(value reflect.Value) (any, error) {
|
||||||
if value.Kind() == reflect.Ptr {
|
if value.Kind() == reflect.Pointer {
|
||||||
return impl.getMapValue(value.Elem())
|
return impl.getMapValue(value.Elem())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,10 +73,16 @@ func (cc *CopyCollector) WriteFile(fpath string, fi fs.FileInfo, linkName string
|
|||||||
if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
|
if err := os.MkdirAll(filepath.Dir(fdestpath), 0o777); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Remove any existing destination so we can overwrite read-only files
|
||||||
|
// (e.g. git pack files at mode 0444 trip EACCES on macOS and "Access is
|
||||||
|
// denied" on Windows when reopened with O_WRONLY) and so os.Symlink does
|
||||||
|
// not fail with EEXIST. os.Remove clears the Windows read-only attribute
|
||||||
|
// internally; on Unix unlink only needs write permission on the parent.
|
||||||
|
_ = os.Remove(fdestpath)
|
||||||
if f == nil {
|
if f == nil {
|
||||||
return os.Symlink(linkName, fdestpath)
|
return os.Symlink(linkName, fdestpath)
|
||||||
}
|
}
|
||||||
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY, fi.Mode())
|
df, err := os.OpenFile(fdestpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fi.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"archive/tar"
|
"archive/tar"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ import (
|
|||||||
"github.com/go-git/go-git/v5/plumbing/format/index"
|
"github.com/go-git/go-git/v5/plumbing/format/index"
|
||||||
"github.com/go-git/go-git/v5/storage/filesystem"
|
"github.com/go-git/go-git/v5/storage/filesystem"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type memoryFs struct {
|
type memoryFs struct {
|
||||||
@@ -174,3 +177,47 @@ func TestSymlinks(t *testing.T) {
|
|||||||
assert.Equal(t, ".env", files["test.env"].Linkname)
|
assert.Equal(t, ".env", files["test.env"].Linkname)
|
||||||
assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF")
|
assert.ErrorIs(t, err, io.EOF, "tar must be read cleanly to EOF")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression for https://gitea.com/gitea/runner/issues/876 and /941:
|
||||||
|
// re-copying an action directory must overwrite a pre-existing read-only
|
||||||
|
// file (e.g. a git pack .idx at mode 0444) instead of failing with EACCES
|
||||||
|
// on macOS or "Access is denied" on Windows.
|
||||||
|
func TestCopyCollectorWriteFileOverwritesReadOnlyFile(t *testing.T) {
|
||||||
|
dst := t.TempDir()
|
||||||
|
target := filepath.Join(dst, "sub", "pack.idx")
|
||||||
|
require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(target, []byte("old"), 0o444))
|
||||||
|
|
||||||
|
src := filepath.Join(t.TempDir(), "pack.idx")
|
||||||
|
require.NoError(t, os.WriteFile(src, []byte("new"), 0o444))
|
||||||
|
fi, err := os.Stat(src)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cc := &CopyCollector{DstDir: dst}
|
||||||
|
require.NoError(t, cc.WriteFile("sub/pack.idx", fi, "", strings.NewReader("new")))
|
||||||
|
|
||||||
|
got, err := os.ReadFile(target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "new", string(got))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without the destination removal, os.Symlink fails with EEXIST when the
|
||||||
|
// path already holds a regular file from an earlier copy of the action.
|
||||||
|
func TestCopyCollectorWriteFileOverwritesFileWithSymlink(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("creating symlinks requires elevated privileges on Windows")
|
||||||
|
}
|
||||||
|
dst := t.TempDir()
|
||||||
|
target := filepath.Join(dst, "link")
|
||||||
|
require.NoError(t, os.WriteFile(target, []byte("stale"), 0o644))
|
||||||
|
|
||||||
|
fi, err := os.Lstat(target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cc := &CopyCollector{DstDir: dst}
|
||||||
|
require.NoError(t, cc.WriteFile("link", fi, "target", nil))
|
||||||
|
|
||||||
|
resolved, err := os.Readlink(target)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "target", resolved)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,18 +4,6 @@
|
|||||||
|
|
||||||
package lookpath
|
package lookpath
|
||||||
|
|
||||||
import "os"
|
|
||||||
|
|
||||||
type Env interface {
|
type Env interface {
|
||||||
Getenv(name string) string
|
Getenv(name string) string
|
||||||
}
|
}
|
||||||
|
|
||||||
type defaultEnv struct{}
|
|
||||||
|
|
||||||
func (*defaultEnv) Getenv(name string) string {
|
|
||||||
return os.Getenv(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func LookPath(file string) (string, error) {
|
|
||||||
return LookPath2(file, &defaultEnv{})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
"gitea.com/gitea/runner/act/container"
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
@@ -44,6 +45,11 @@ type runAction func(step actionStep, actionDir string, remoteAction *remoteActio
|
|||||||
//go:embed res/trampoline.js
|
//go:embed res/trampoline.js
|
||||||
var trampoline embed.FS
|
var trampoline embed.FS
|
||||||
|
|
||||||
|
var (
|
||||||
|
ContainerImageExistsLocally = container.ImageExistsLocally
|
||||||
|
ContainerNewDockerBuildExecutor = container.NewDockerBuildExecutor
|
||||||
|
)
|
||||||
|
|
||||||
func readActionImpl(ctx context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
func readActionImpl(ctx context.Context, step *model.Step, actionDir, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
allErrors := []error{}
|
allErrors := []error{}
|
||||||
@@ -148,6 +154,8 @@ func maybeCopyToActionDir(ctx context.Context, step actionStep, actionDir, actio
|
|||||||
return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
|
return rc.JobContainer.CopyTarStream(ctx, containerActionDirCopy, ta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer git.AcquireCloneLock(actionDir)()
|
||||||
|
|
||||||
if err := removeGitIgnore(ctx, actionDir); err != nil {
|
if err := removeGitIgnore(ctx, actionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -197,7 +205,7 @@ func runActionImpl(step actionStep, actionDir string, remoteAction *remoteAction
|
|||||||
if remoteAction == nil {
|
if remoteAction == nil {
|
||||||
location = containerActionDir
|
location = containerActionDir
|
||||||
}
|
}
|
||||||
return execAsDocker(ctx, step, actionName, location, remoteAction == nil)
|
return execAsDocker(ctx, step, actionName, actionDir, location, remoteAction == nil)
|
||||||
case x.IsComposite():
|
case x.IsComposite():
|
||||||
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
if err := maybeCopyToActionDir(ctx, step, actionDir, actionPath, containerActionDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -265,7 +273,7 @@ func removeGitIgnore(ctx context.Context, directory string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: break out parts of function to reduce complexicity
|
// TODO: break out parts of function to reduce complexicity
|
||||||
func execAsDocker(ctx context.Context, step actionStep, actionName, basedir string, localAction bool) error {
|
func execAsDocker(ctx context.Context, step actionStep, actionName, actionDir, basedir string, localAction bool) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rc := step.getRunContext()
|
rc := step.getRunContext()
|
||||||
action := step.getActionModel()
|
action := step.getActionModel()
|
||||||
@@ -284,12 +292,12 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, basedir stri
|
|||||||
image = strings.ToLower(image)
|
image = strings.ToLower(image)
|
||||||
contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
|
contextDir, fileName := filepath.Split(filepath.Join(basedir, action.Runs.Image))
|
||||||
|
|
||||||
anyArchExists, err := container.ImageExistsLocally(ctx, image, "any")
|
anyArchExists, err := ContainerImageExistsLocally(ctx, image, "any")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
correctArchExists, err := container.ImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
correctArchExists, err := ContainerImageExistsLocally(ctx, image, rc.Config.ContainerArchitecture)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -321,13 +329,21 @@ func execAsDocker(ctx context.Context, step actionStep, actionName, basedir stri
|
|||||||
}
|
}
|
||||||
defer buildContext.Close()
|
defer buildContext.Close()
|
||||||
}
|
}
|
||||||
prepImage = container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
prepImage = ContainerNewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||||
ContextDir: contextDir,
|
ContextDir: contextDir,
|
||||||
Dockerfile: fileName,
|
Dockerfile: fileName,
|
||||||
ImageTag: image,
|
ImageTag: image,
|
||||||
BuildContext: buildContext,
|
BuildContext: buildContext,
|
||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
})
|
})
|
||||||
|
if buildContext == nil {
|
||||||
|
// Held across the whole build: the daemon drains contextDir lazily.
|
||||||
|
inner := prepImage
|
||||||
|
prepImage = func(ctx context.Context) error {
|
||||||
|
defer git.AcquireCloneLock(actionDir)()
|
||||||
|
return inner(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
|
logger.Debugf("image '%s' for architecture '%s' already exists", image, rc.Config.ContainerArchitecture)
|
||||||
}
|
}
|
||||||
@@ -440,6 +456,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
|||||||
Options: rc.Config.ContainerOptions,
|
Options: rc.Config.ContainerOptions,
|
||||||
AutoRemove: rc.Config.AutoRemove,
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
ValidVolumes: rc.Config.ValidVolumes,
|
ValidVolumes: rc.Config.ValidVolumes,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
return stepContainer
|
return stepContainer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2024 The nektos/act Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package runner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
git "github.com/go-git/go-git/v5"
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GoGitActionCacheOfflineMode struct {
|
|
||||||
Parent GoGitActionCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c GoGitActionCacheOfflineMode) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
|
|
||||||
sha, fetchErr := c.Parent.Fetch(ctx, cacheDir, url, ref, token)
|
|
||||||
gitPath := path.Join(c.Parent.Path, safeFilename(cacheDir)+".git")
|
|
||||||
gogitrepo, err := git.PlainOpen(gitPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fetchErr
|
|
||||||
}
|
|
||||||
refName := plumbing.ReferenceName("refs/action-cache-offline/" + ref)
|
|
||||||
r, err := gogitrepo.Reference(refName, true)
|
|
||||||
if fetchErr == nil {
|
|
||||||
if err != nil || sha != r.Hash().String() {
|
|
||||||
if err == nil {
|
|
||||||
refName = r.Name()
|
|
||||||
}
|
|
||||||
ref := plumbing.NewHashReference(refName, plumbing.NewHash(sha))
|
|
||||||
_ = gogitrepo.Storer.SetReference(ref)
|
|
||||||
}
|
|
||||||
} else if err == nil {
|
|
||||||
return r.Hash().String(), nil
|
|
||||||
}
|
|
||||||
return sha, fetchErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c GoGitActionCacheOfflineMode) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
|
|
||||||
return c.Parent.GetTarArchive(ctx, cacheDir, sha, includePrefix)
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,13 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
|
"gitea.com/gitea/runner/act/container"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -252,3 +257,153 @@ func TestActionRunner(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMaybeCopyToActionDirHoldsCloneLock(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
actionDir := t.TempDir()
|
||||||
|
|
||||||
|
releaseCopy := make(chan struct{})
|
||||||
|
release := sync.OnceFunc(func() { close(releaseCopy) })
|
||||||
|
defer release()
|
||||||
|
|
||||||
|
copyEntered := make(chan struct{})
|
||||||
|
|
||||||
|
cm := &containerMock{}
|
||||||
|
cm.On("CopyDir", "/var/run/act/actions/", actionDir+"/", false).Return(func(ctx context.Context) error {
|
||||||
|
close(copyEntered)
|
||||||
|
<-releaseCopy
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
step := &stepActionRemote{
|
||||||
|
Step: &model.Step{Uses: "remote/action@v1"},
|
||||||
|
RunContext: &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
JobContainer: cm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
copyDone := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
copyDone <- maybeCopyToActionDir(ctx, step, actionDir, "", "/var/run/act/actions/")
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-copyEntered:
|
||||||
|
case err := <-copyDone:
|
||||||
|
t.Fatalf("maybeCopyToActionDir returned before CopyDir was entered: %v", err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("CopyDir was not entered within 1 second")
|
||||||
|
}
|
||||||
|
|
||||||
|
peerAcquired := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
unlock := git.AcquireCloneLock(actionDir)
|
||||||
|
close(peerAcquired)
|
||||||
|
unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-peerAcquired:
|
||||||
|
t.Fatal("peer AcquireCloneLock returned while CopyDir was running")
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
release()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-copyDone:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("maybeCopyToActionDir returned error: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("maybeCopyToActionDir did not return after CopyDir was unblocked")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-peerAcquired:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("peer AcquireCloneLock did not proceed after lock released")
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecAsDockerHoldsCloneLockForRemoteUncached(t *testing.T) {
|
||||||
|
actionDir := t.TempDir()
|
||||||
|
|
||||||
|
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(actionDir))
|
||||||
|
defer unlockOnce()
|
||||||
|
|
||||||
|
innerEntered := make(chan struct{})
|
||||||
|
releaseInner := make(chan struct{})
|
||||||
|
releaseOnce := sync.OnceFunc(func() { close(releaseInner) })
|
||||||
|
defer releaseOnce()
|
||||||
|
|
||||||
|
origImageExists := ContainerImageExistsLocally
|
||||||
|
ContainerImageExistsLocally = func(_ context.Context, _, _ string) (bool, error) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
defer func() { ContainerImageExistsLocally = origImageExists }()
|
||||||
|
|
||||||
|
origBuildExec := ContainerNewDockerBuildExecutor
|
||||||
|
ContainerNewDockerBuildExecutor = func(_ container.NewDockerBuildExecutorInput) common.Executor {
|
||||||
|
return func(_ context.Context) error {
|
||||||
|
close(innerEntered)
|
||||||
|
<-releaseInner
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() { ContainerNewDockerBuildExecutor = origBuildExec }()
|
||||||
|
|
||||||
|
step := &stepActionRemote{
|
||||||
|
Step: &model.Step{ID: "1", Uses: "remote/action@v1", With: map[string]string{}},
|
||||||
|
RunContext: &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "1",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Name: "wf",
|
||||||
|
Jobs: map[string]*model.Job{"1": {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JobContainer: &containerMock{},
|
||||||
|
},
|
||||||
|
action: &model.Action{Runs: model.ActionRuns{Using: "docker", Image: "Dockerfile"}},
|
||||||
|
env: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- execAsDocker(ctx, step, "test-action", actionDir, actionDir, false) }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-innerEntered:
|
||||||
|
t.Fatal("inner build executor ran before clone lock was released")
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatalf("execAsDocker returned before inner was entered: %v", err)
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockOnce()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-innerEntered:
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatalf("execAsDocker returned without entering inner: %v", err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("inner build executor not entered after lock released")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
releaseOnce()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("execAsDocker did not return after inner was released and ctx was canceled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (rc *RunContext) setOutput(ctx context.Context, kvPairs map[string]string,
|
|||||||
|
|
||||||
result, ok := rc.StepResults[stepID]
|
result, ok := rc.StepResults[stepID]
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Infof(" \U00002757 no outputs used step '%s'", stepID)
|
logger.Infof("No outputs registered for step '%s'", stepID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -142,9 +142,16 @@ func cloneRemoteReusableWorkflow(rc *RunContext, cloneURL, ref, targetDirectory,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var modelNewWorkflowPlanner = model.NewWorkflowPlanner
|
||||||
|
|
||||||
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) common.Executor {
|
func newReusableWorkflowExecutor(rc *RunContext, directory, workflow string) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
|
// Scoped to the yaml read so concurrent invocations don't serialize
|
||||||
|
// on the whole job run.
|
||||||
|
planner, err := func() (model.WorkflowPlanner, error) {
|
||||||
|
defer git.AcquireCloneLock(directory)()
|
||||||
|
return modelNewWorkflowPlanner(path.Join(directory, workflow), true)
|
||||||
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -278,7 +285,7 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner Runner) common.Executo
|
|||||||
rc.caller.setReusedWorkflowJobResult(rc.JobName, reusedWorkflowJobResult)
|
rc.caller.setReusedWorkflowJobResult(rc.JobName, reusedWorkflowJobResult)
|
||||||
} else {
|
} else {
|
||||||
rc.result(reusedWorkflowJobResult)
|
rc.result(reusedWorkflowJobResult)
|
||||||
logger.WithField("jobResult", reusedWorkflowJobResult).Infof("\U0001F3C1 Job %s", reusedWorkflowJobResultMessage)
|
logger.WithField("jobResult", reusedWorkflowJobResult).Infof("Job %s", reusedWorkflowJobResultMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +308,11 @@ func getGitCloneToken(conf *Config, cloneURL string) string {
|
|||||||
// 1. cloneURL is from the same Gitea instance that the runner is registered to
|
// 1. cloneURL is from the same Gitea instance that the runner is registered to
|
||||||
// 2. the cloneURL does not have basic auth embedded
|
// 2. the cloneURL does not have basic auth embedded
|
||||||
func shouldCloneURLUseToken(instanceURL, cloneURL string) bool {
|
func shouldCloneURLUseToken(instanceURL, cloneURL string) bool {
|
||||||
|
if !strings.HasPrefix(instanceURL, "http://") &&
|
||||||
|
!strings.HasPrefix(instanceURL, "https://") {
|
||||||
|
instanceURL = "https://" + instanceURL
|
||||||
|
}
|
||||||
|
|
||||||
u1, err1 := url.Parse(instanceURL)
|
u1, err1 := url.Parse(instanceURL)
|
||||||
u2, err2 := url.Parse(cloneURL)
|
u2, err2 := url.Parse(cloneURL)
|
||||||
if err1 != nil || err2 != nil {
|
if err1 != nil || err2 != nil {
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common/git"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -71,6 +75,113 @@ func TestReusableWorkflowCachedBranchRefRefreshes(t *testing.T) {
|
|||||||
require.Equal(t, tmpl("v2"), string(got), "cached workflow file must reflect the updated branch tip")
|
require.Equal(t, tmpl("v2"), string(got), "cached workflow file must reflect the updated branch tip")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewReusableWorkflowExecutorHoldsCloneLock(t *testing.T) {
|
||||||
|
workflowDir := t.TempDir()
|
||||||
|
|
||||||
|
unlockOnce := sync.OnceFunc(git.AcquireCloneLock(workflowDir))
|
||||||
|
defer unlockOnce()
|
||||||
|
|
||||||
|
plannerCalled := make(chan struct{})
|
||||||
|
|
||||||
|
origPlanner := modelNewWorkflowPlanner
|
||||||
|
modelNewWorkflowPlanner = func(string, bool) (model.WorkflowPlanner, error) {
|
||||||
|
close(plannerCalled)
|
||||||
|
return nil, errors.New("stop")
|
||||||
|
}
|
||||||
|
defer func() { modelNewWorkflowPlanner = origPlanner }()
|
||||||
|
|
||||||
|
rc := &RunContext{
|
||||||
|
Config: &Config{},
|
||||||
|
Run: &model.Run{Workflow: &model.Workflow{Jobs: map[string]*model.Job{}}},
|
||||||
|
}
|
||||||
|
exec := newReusableWorkflowExecutor(rc, workflowDir, "reusable.yml")
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() { done <- exec(context.Background()) }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-plannerCalled:
|
||||||
|
t.Fatal("planner ran while clone lock was held")
|
||||||
|
case err := <-done:
|
||||||
|
t.Fatalf("executor returned before planner was reached: %v", err)
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockOnce()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-plannerCalled:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("planner not called after lock was released")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
require.Error(t, err)
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("executor did not return after planner ran")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetGitCloneTokenWithSchemalessGiteaInstance(t *testing.T) {
|
||||||
|
conf := &Config{
|
||||||
|
GitHubInstance: "gitea.example.net",
|
||||||
|
Secrets: map[string]string{
|
||||||
|
"GITEA_TOKEN": "token-value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := getGitCloneToken(conf, "https://gitea.example.net/actions/tools")
|
||||||
|
|
||||||
|
require.Equal(t, "token-value", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCloneURLUseToken(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
instanceURL string
|
||||||
|
cloneURL string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "same host with schemaless instance",
|
||||||
|
instanceURL: "gitea.example.net",
|
||||||
|
cloneURL: "https://gitea.example.net/actions/tools",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same host with schemaless instance and port",
|
||||||
|
instanceURL: "gitea.example.net:3000",
|
||||||
|
cloneURL: "https://gitea.example.net:3000/actions/tools",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different host",
|
||||||
|
instanceURL: "gitea.example.net",
|
||||||
|
cloneURL: "https://github.com/actions/tools",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "embedded basic auth",
|
||||||
|
instanceURL: "gitea.example.net",
|
||||||
|
cloneURL: "https://user:pass@gitea.example.net/actions/tools",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid clone URL",
|
||||||
|
instanceURL: "gitea.example.net",
|
||||||
|
cloneURL: "://gitea.example.net/actions/tools",
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
require.Equal(t, tt.want, shouldCloneURLUseToken(tt.instanceURL, tt.cloneURL))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func gitMust(t *testing.T, dir string, args ...string) {
|
func gitMust(t *testing.T, dir string, args ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cmd := exec.Command("git", args...)
|
cmd := exec.Command("git", args...)
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
|
|||||||
func (rc *RunContext) startHostEnvironment() common.Executor {
|
func (rc *RunContext) startHostEnvironment() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
rawLogger := logger.WithField("raw_output", true)
|
rawLogger := logger.WithField(rawOutputField, true)
|
||||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||||
if rc.Config.LogOutput {
|
if rc.Config.LogOutput {
|
||||||
rawLogger.Infof("%s", s)
|
rawLogger.Infof("%s", s)
|
||||||
@@ -224,12 +224,13 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
|||||||
TmpDir: runnerTmp,
|
TmpDir: runnerTmp,
|
||||||
ToolCache: toolCache,
|
ToolCache: toolCache,
|
||||||
Workdir: rc.Config.Workdir,
|
Workdir: rc.Config.Workdir,
|
||||||
BindWorkdir: rc.Config.BindWorkdir,
|
CleanWorkdir: rc.Config.CleanWorkdir,
|
||||||
ActPath: actPath,
|
ActPath: actPath,
|
||||||
CleanUp: func() {
|
CleanUp: func() {
|
||||||
os.RemoveAll(miscpath)
|
os.RemoveAll(miscpath)
|
||||||
},
|
},
|
||||||
StdOut: logWriter,
|
StdOut: logWriter,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
}
|
}
|
||||||
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
rc.cleanUpJobContainer = rc.JobContainer.Remove()
|
||||||
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
for k, v := range rc.JobContainer.GetRunnerContext(ctx) {
|
||||||
@@ -260,11 +261,24 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printStartJobContainerGroup mirrors actions/runner's "Starting job container"
|
||||||
|
// section: emit the group header and summary, return a closer for ::endgroup::.
|
||||||
|
func printStartJobContainerGroup(ctx context.Context, image, name, network string) func() {
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
rawLogger.Infof("::group::Starting job container")
|
||||||
|
rawLogger.Infof("image: %s", image)
|
||||||
|
rawLogger.Infof("name: %s", name)
|
||||||
|
rawLogger.Infof("network: %s", network)
|
||||||
|
return func() {
|
||||||
|
rawLogger.Infof("::endgroup::")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (rc *RunContext) startJobContainer() common.Executor {
|
func (rc *RunContext) startJobContainer() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
image := rc.platformImage(ctx)
|
image := rc.platformImage(ctx)
|
||||||
rawLogger := logger.WithField("raw_output", true)
|
rawLogger := logger.WithField(rawOutputField, true)
|
||||||
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
logWriter := common.NewLineWriter(rc.commandHandler(ctx), func(s string) bool {
|
||||||
if rc.Config.LogOutput {
|
if rc.Config.LogOutput {
|
||||||
rawLogger.Infof("%s", s)
|
rawLogger.Infof("%s", s)
|
||||||
@@ -279,7 +293,6 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
return fmt.Errorf("failed to handle credentials: %s", err)
|
return fmt.Errorf("failed to handle credentials: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("\U0001f680 Start image=%s", image)
|
|
||||||
name := rc.jobContainerName()
|
name := rc.jobContainerName()
|
||||||
// For gitea, to support --volumes-from <container_name_or_id> in options.
|
// For gitea, to support --volumes-from <container_name_or_id> in options.
|
||||||
// We need to set the container name to the environment variable.
|
// We need to set the container name to the environment variable.
|
||||||
@@ -359,6 +372,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
NetworkAliases: []string{serviceID},
|
NetworkAliases: []string{serviceID},
|
||||||
ExposedPorts: exposedPorts,
|
ExposedPorts: exposedPorts,
|
||||||
PortBindings: portBindings,
|
PortBindings: portBindings,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
rc.ServiceContainers = append(rc.ServiceContainers, c)
|
rc.ServiceContainers = append(rc.ServiceContainers, c)
|
||||||
}
|
}
|
||||||
@@ -419,11 +433,13 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
Options: rc.options(ctx),
|
Options: rc.options(ctx),
|
||||||
AutoRemove: rc.Config.AutoRemove,
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
ValidVolumes: rc.Config.ValidVolumes,
|
ValidVolumes: rc.Config.ValidVolumes,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
if rc.JobContainer == nil {
|
if rc.JobContainer == nil {
|
||||||
return errors.New("Failed to create job container")
|
return errors.New("Failed to create job container")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer printStartJobContainerGroup(ctx, image, name, networkName)()
|
||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
rc.pullServicesImages(rc.Config.ForcePull),
|
rc.pullServicesImages(rc.Config.ForcePull),
|
||||||
rc.JobContainer.Pull(rc.Config.ForcePull),
|
rc.JobContainer.Pull(rc.Config.ForcePull),
|
||||||
@@ -585,10 +601,34 @@ func (rc *RunContext) interpolateOutputs() common.Executor {
|
|||||||
|
|
||||||
func (rc *RunContext) startContainer() common.Executor {
|
func (rc *RunContext) startContainer() common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
|
var err error
|
||||||
if rc.IsHostEnv(ctx) {
|
if rc.IsHostEnv(ctx) {
|
||||||
return rc.startHostEnvironment()(ctx)
|
err = rc.startHostEnvironment()(ctx)
|
||||||
|
} else {
|
||||||
|
err = rc.startJobContainer()(ctx)
|
||||||
}
|
}
|
||||||
return rc.startJobContainer()(ctx)
|
if err != nil {
|
||||||
|
// The job executor's teardown only runs after a successful start, so a failed
|
||||||
|
// start would otherwise leak the per-job network and container.
|
||||||
|
rc.cleanupFailedStart(ctx)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RunContext) cleanupFailedStart(ctx context.Context) {
|
||||||
|
if rc.cleanUpJobContainer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cleanCtx := ctx
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
// the start likely failed because ctx was cancelled, detach so teardown still runs
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
cleanCtx, cancel = context.WithTimeout(common.WithLogger(context.Background(), common.Logger(ctx)), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
if err := rc.cleanUpJobContainer(cleanCtx); err != nil {
|
||||||
|
common.Logger(ctx).Errorf("Error while cleaning up after failed container start for job %s: %v", rc.JobName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,7 +793,7 @@ func (rc *RunContext) isEnabled(ctx context.Context) (bool, error) {
|
|||||||
img := rc.platformImage(ctx)
|
img := rc.platformImage(ctx)
|
||||||
if img == "" {
|
if img == "" {
|
||||||
for _, platformName := range rc.runsOnPlatformNames(ctx) {
|
for _, platformName := range rc.runsOnPlatformNames(ctx) {
|
||||||
l.Infof("\U0001F6A7 Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
|
l.Infof("Skipping unsupported platform -- Try running with `-P %+v=...`", platformName)
|
||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,11 +13,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/exprparser"
|
"gitea.com/gitea/runner/act/exprparser"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
|
require "github.com/stretchr/testify/require"
|
||||||
yaml "go.yaml.in/yaml/v4"
|
yaml "go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -635,3 +638,75 @@ func TestCreateContainerNameBoundedForLongMatrixInput(t *testing.T) {
|
|||||||
assert.LessOrEqual(t, len(name+"-network"), 255)
|
assert.LessOrEqual(t, len(name+"-network"), 255)
|
||||||
assert.LessOrEqual(t, len(name+"-job1234567890"), 255)
|
assert.LessOrEqual(t, len(name+"-job1234567890"), 255)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrintStartJobContainerGroupGolden(t *testing.T) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
logger := log.New()
|
||||||
|
logger.SetOutput(buf)
|
||||||
|
logger.SetLevel(log.InfoLevel)
|
||||||
|
logger.SetFormatter(&jobLogFormatter{color: cyan})
|
||||||
|
entry := logger.WithFields(log.Fields{"job": "j1"})
|
||||||
|
ctx := common.WithLogger(context.Background(), entry)
|
||||||
|
|
||||||
|
printStartJobContainerGroup(ctx, "node:20", "GITEA-WORKFLOW-build-JOB-test", "gitea-runner-network")()
|
||||||
|
|
||||||
|
want := strings.Join([]string{
|
||||||
|
"[j1] | ::group::Starting job container",
|
||||||
|
"[j1] | image: node:20",
|
||||||
|
"[j1] | name: GITEA-WORKFLOW-build-JOB-test",
|
||||||
|
"[j1] | network: gitea-runner-network",
|
||||||
|
"[j1] | ::endgroup::",
|
||||||
|
"",
|
||||||
|
}, "\n")
|
||||||
|
assert.Equal(t, want, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunContext_cleanupFailedStart(t *testing.T) {
|
||||||
|
type ctxKey string
|
||||||
|
const sentinel = ctxKey("sentinel")
|
||||||
|
|
||||||
|
// the fresh context is cancelled via defer on return, so capture state inside the stub
|
||||||
|
type capture struct {
|
||||||
|
calls int
|
||||||
|
err error
|
||||||
|
sentinel any
|
||||||
|
}
|
||||||
|
newRC := func(c *capture) *RunContext {
|
||||||
|
return &RunContext{
|
||||||
|
JobName: "job",
|
||||||
|
cleanUpJobContainer: func(ctx context.Context) error {
|
||||||
|
c.calls++
|
||||||
|
c.err = ctx.Err()
|
||||||
|
c.sentinel = ctx.Value(sentinel)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("runs teardown on the live context", func(t *testing.T) {
|
||||||
|
var c capture
|
||||||
|
ctx := context.WithValue(context.Background(), sentinel, "v")
|
||||||
|
|
||||||
|
newRC(&c).cleanupFailedStart(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, c.calls)
|
||||||
|
require.NoError(t, c.err)
|
||||||
|
assert.Equal(t, "v", c.sentinel)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("falls back to a fresh context when the input is done", func(t *testing.T) {
|
||||||
|
var c capture
|
||||||
|
ctx, cancel := context.WithCancel(context.WithValue(context.Background(), sentinel, "v"))
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
newRC(&c).cleanupFailedStart(ctx)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, c.calls)
|
||||||
|
require.NoError(t, c.err)
|
||||||
|
assert.Nil(t, c.sentinel)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no-op when there is nothing to clean up", func(t *testing.T) {
|
||||||
|
assert.NotPanics(t, func() { (&RunContext{}).cleanupFailedStart(context.Background()) })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
|
|
||||||
docker_container "github.com/docker/docker/api/types/container"
|
docker_container "github.com/moby/moby/api/types/container"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ type Config struct {
|
|||||||
Actor string // the user that triggered the event
|
Actor string // the user that triggered the event
|
||||||
Workdir string // path to working directory
|
Workdir string // path to working directory
|
||||||
ActionCacheDir string // path used for caching action contents
|
ActionCacheDir string // path used for caching action contents
|
||||||
ActionOfflineMode bool // when offline, use caching action contents
|
ActionOfflineMode bool // when offline, use cached action contents
|
||||||
BindWorkdir bool // bind the workdir to the job container
|
BindWorkdir bool // bind the workdir to the job container
|
||||||
EventName string // name of event to run
|
EventName string // name of event to run
|
||||||
EventPath string // path to JSON file to use for event.json in containers
|
EventPath string // path to JSON file to use for event.json in containers
|
||||||
@@ -73,12 +73,14 @@ type Config struct {
|
|||||||
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
|
||||||
ContainerNamePrefix string // the prefix of container name
|
ContainerNamePrefix string // the prefix of container name
|
||||||
ContainerMaxLifetime time.Duration // the max lifetime of job containers
|
ContainerMaxLifetime time.Duration // the max lifetime of job containers
|
||||||
|
CleanWorkdir bool // remove host executor workdir on teardown
|
||||||
DefaultActionInstance string // the default actions web site
|
DefaultActionInstance string // the default actions web site
|
||||||
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
|
PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil
|
||||||
JobLoggerLevel *log.Level // the level of job logger
|
JobLoggerLevel *log.Level // the level of job logger
|
||||||
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
|
ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers
|
||||||
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance
|
||||||
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count)
|
||||||
|
AllocatePTY bool // allocate a pseudo-TTY for each step's process
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetToken: Adapt to Gitea
|
// GetToken: Adapt to Gitea
|
||||||
@@ -90,6 +92,17 @@ func (c Config) GetToken() string {
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultActionURL returns the host used for implicit remote actions.
|
||||||
|
func (c Config) DefaultActionURL() string {
|
||||||
|
if c.DefaultActionInstance != "" {
|
||||||
|
return c.DefaultActionInstance
|
||||||
|
}
|
||||||
|
if c.GitHubInstance != "" {
|
||||||
|
return c.GitHubInstance
|
||||||
|
}
|
||||||
|
return "github.com"
|
||||||
|
}
|
||||||
|
|
||||||
type caller struct {
|
type caller struct {
|
||||||
runContext *RunContext
|
runContext *RunContext
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.com/gitea/runner/act/common"
|
"gitea.com/gitea/runner/act/common"
|
||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
@@ -192,6 +193,7 @@ func (j *TestJobFileInfo) runTest(ctx context.Context, t *testing.T, cfg *Config
|
|||||||
Inputs: cfg.Inputs,
|
Inputs: cfg.Inputs,
|
||||||
GitHubInstance: "github.com",
|
GitHubInstance: "github.com",
|
||||||
ContainerArchitecture: cfg.ContainerArchitecture,
|
ContainerArchitecture: cfg.ContainerArchitecture,
|
||||||
|
ContainerMaxLifetime: time.Hour,
|
||||||
Matrix: cfg.Matrix,
|
Matrix: cfg.Matrix,
|
||||||
ActionCache: cfg.ActionCache,
|
ActionCache: cfg.ActionCache,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,9 +113,10 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), sar.Step.UsesHash())
|
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), sar.Step.UsesHash())
|
||||||
token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance))
|
defaultActionURL := sar.RunContext.Config.DefaultActionURL()
|
||||||
|
token := getGitCloneToken(sar.getRunContext().Config, sar.remoteAction.CloneURL(defaultActionURL))
|
||||||
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
|
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
|
||||||
URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance),
|
URL: sar.remoteAction.CloneURL(defaultActionURL),
|
||||||
Ref: sar.remoteAction.Ref,
|
Ref: sar.remoteAction.Ref,
|
||||||
Dir: actionDir,
|
Dir: actionDir,
|
||||||
Token: token,
|
Token: token,
|
||||||
@@ -145,6 +146,7 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
|
|||||||
return common.NewPipelineExecutor(
|
return common.NewPipelineExecutor(
|
||||||
ntErr,
|
ntErr,
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
|
defer git.AcquireCloneLock(actionDir)()
|
||||||
actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
|
actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
|
||||||
sar.action = actionModel
|
sar.action = actionModel
|
||||||
return err
|
return err
|
||||||
@@ -273,7 +275,7 @@ func (sar *stepActionRemote) cloneSkipTLS() bool {
|
|||||||
if sar.remoteAction.URL == "" {
|
if sar.remoteAction.URL == "" {
|
||||||
// Empty URL means the default action instance should be used
|
// Empty URL means the default action instance should be used
|
||||||
// Return true if the URL of the Gitea instance is the same as the URL of the default action instance
|
// Return true if the URL of the Gitea instance is the same as the URL of the default action instance
|
||||||
return sar.RunContext.Config.DefaultActionInstance == sar.RunContext.Config.GitHubInstance
|
return sar.RunContext.Config.DefaultActionURL() == sar.RunContext.Config.GitHubInstance
|
||||||
}
|
}
|
||||||
// Return true if the URL of the remote action is the same as the URL of the Gitea instance
|
// Return true if the URL of the remote action is the same as the URL of the Gitea instance
|
||||||
return sar.remoteAction.URL == sar.RunContext.Config.GitHubInstance
|
return sar.remoteAction.URL == sar.RunContext.Config.GitHubInstance
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -434,6 +435,57 @@ func TestStepActionRemotePreThroughActionToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStepActionRemoteUsesGitHubInstanceWhenDefaultActionInstanceEmpty(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var actualURL string
|
||||||
|
sarm := &stepActionRemoteMocks{}
|
||||||
|
|
||||||
|
origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
|
||||||
|
stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
actualURL = input.URL
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
|
||||||
|
}()
|
||||||
|
|
||||||
|
sar := &stepActionRemote{
|
||||||
|
Step: &model.Step{
|
||||||
|
Uses: "actions/setup-go@v4",
|
||||||
|
},
|
||||||
|
RunContext: &RunContext{
|
||||||
|
Config: &Config{
|
||||||
|
GitHubInstance: "gitea.example",
|
||||||
|
DefaultActionInstance: "",
|
||||||
|
ActionCacheDir: t.TempDir(),
|
||||||
|
},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "1",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
readAction: sarm.readAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
suffixMatcher := func(suffix string) any {
|
||||||
|
return mock.MatchedBy(func(actionDir string) bool {
|
||||||
|
return strings.HasSuffix(actionDir, suffix)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sarm.On("readAction", sar.Step, suffixMatcher(sar.Step.UsesHash()), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
|
||||||
|
|
||||||
|
require.NoError(t, sar.prepareActionExecutor()(ctx))
|
||||||
|
assert.Equal(t, "https://gitea.example/actions/setup-go", actualURL)
|
||||||
|
sarm.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStepActionRemotePost(t *testing.T) {
|
func TestStepActionRemotePost(t *testing.T) {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ func (sd *stepDocker) newStepContainer(ctx context.Context, image string, cmd, e
|
|||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
AutoRemove: rc.Config.AutoRemove,
|
AutoRemove: rc.Config.AutoRemove,
|
||||||
ValidVolumes: rc.Config.ValidVolumes,
|
ValidVolumes: rc.Config.ValidVolumes,
|
||||||
|
AllocatePTY: rc.Config.AllocatePTY,
|
||||||
})
|
})
|
||||||
return stepContainer
|
return stepContainer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,55 @@ func TestStepDockerMain(t *testing.T) {
|
|||||||
cm.AssertExpectations(t)
|
cm.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStepDockerNewStepContainerAllocatePTY(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
allocPTY bool
|
||||||
|
}{
|
||||||
|
{name: "off", allocPTY: false},
|
||||||
|
{name: "on", allocPTY: true},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
cm := &containerMock{}
|
||||||
|
|
||||||
|
var captured *container.NewContainerInput
|
||||||
|
origContainerNewContainer := ContainerNewContainer
|
||||||
|
ContainerNewContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
|
||||||
|
captured = input
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
ContainerNewContainer = origContainerNewContainer
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
sd := &stepDocker{
|
||||||
|
RunContext: &RunContext{
|
||||||
|
StepResults: map[string]*model.StepResult{},
|
||||||
|
Config: &Config{
|
||||||
|
AllocatePTY: tc.allocPTY,
|
||||||
|
PlatformPicker: func(_ []string) string {
|
||||||
|
return "node:14"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Run: &model.Run{
|
||||||
|
JobID: "1",
|
||||||
|
Workflow: &model.Workflow{
|
||||||
|
Jobs: map[string]*model.Job{"1": {}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JobContainer: cm,
|
||||||
|
},
|
||||||
|
Step: &model.Step{ID: "1", Uses: "docker://node:14"},
|
||||||
|
}
|
||||||
|
sd.RunContext.ExprEval = sd.RunContext.NewExpressionEvaluator(ctx)
|
||||||
|
|
||||||
|
_ = sd.newStepContainer(ctx, "node:14", []string{"echo", "hi"}, nil)
|
||||||
|
assert.Equal(t, tc.allocPTY, captured.AllocatePTY)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStepDockerPrePost(t *testing.T) {
|
func TestStepDockerPrePost(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
sd := &stepDocker{}
|
sd := &stepDocker{}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3
|
FROM alpine:3.23
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ outputs:
|
|||||||
description: 'The time we greeted you'
|
description: 'The time we greeted you'
|
||||||
runs:
|
runs:
|
||||||
using: 'node24'
|
using: 'node24'
|
||||||
main: 'dist/index.js'
|
main: 'index.js'
|
||||||
|
|||||||
21
act/runner/testdata/actions/node24/index.js
vendored
21
act/runner/testdata/actions/node24/index.js
vendored
@@ -1,11 +1,14 @@
|
|||||||
import {getInput, setOutput, setFailed} from '@actions/core';
|
import {appendFileSync, readFileSync} from 'node:fs';
|
||||||
import {context} from '@actions/github';
|
|
||||||
|
|
||||||
try {
|
const nameToGreet = process.env['INPUT_WHO-TO-GREET'] || 'World';
|
||||||
const nameToGreet = getInput('who-to-greet');
|
console.log(`Hello ${nameToGreet}!`);
|
||||||
console.log(`Hello ${nameToGreet}!`);
|
|
||||||
setOutput('time', (new Date()).toTimeString());
|
if (process.env.GITHUB_OUTPUT) {
|
||||||
console.log(`The event payload: ${JSON.stringify(context.payload, undefined, 2)}`);
|
appendFileSync(process.env.GITHUB_OUTPUT, `time=${new Date().toTimeString()}\n`);
|
||||||
} catch (error) {
|
|
||||||
setFailed(error.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let payload = {};
|
||||||
|
if (process.env.GITHUB_EVENT_PATH) {
|
||||||
|
payload = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
|
||||||
|
}
|
||||||
|
console.log(`The event payload: ${JSON.stringify(payload, undefined, 2)}`);
|
||||||
|
|||||||
20
act/runner/testdata/actions/node24/package.json
vendored
20
act/runner/testdata/actions/node24/package.json
vendored
@@ -1,21 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "node24",
|
"name": "node24",
|
||||||
"version": "1.0.0",
|
"private": true,
|
||||||
"description": "",
|
"type": "module"
|
||||||
"main": "index.js",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "ncc build index.js"
|
|
||||||
},
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@actions/core": "^3.0.1",
|
|
||||||
"@actions/github": "^9.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@vercel/ncc": "^0.38.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=24"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
act/runner/testdata/secrets/.env
vendored
Normal file
2
act/runner/testdata/secrets/.env
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
HELLO=WORLD
|
||||||
|
MULTILINE_ENV="foo\nbar\nbaz"
|
||||||
@@ -9,9 +9,9 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '24'
|
||||||
- run: |
|
- run: |
|
||||||
console.log(process.version);
|
console.log(process.version);
|
||||||
console.log("Hi from node");
|
console.log("Hi from node");
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2023 The nektos/act Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package workflowpattern
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type TraceWriter interface {
|
|
||||||
Info(string, ...any)
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmptyTraceWriter struct{}
|
|
||||||
|
|
||||||
func (*EmptyTraceWriter) Info(string, ...any) {
|
|
||||||
}
|
|
||||||
|
|
||||||
type StdOutTraceWriter struct{}
|
|
||||||
|
|
||||||
func (*StdOutTraceWriter) Info(format string, args ...any) {
|
|
||||||
fmt.Printf(format+"\n", args...) //nolint:forbidigo // pre-existing issue from nektos/act
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2023 The nektos/act Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package workflowpattern
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WorkflowPattern struct {
|
|
||||||
Pattern string
|
|
||||||
Negative bool
|
|
||||||
Regex *regexp.Regexp
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompilePattern(rawpattern string) (*WorkflowPattern, error) {
|
|
||||||
negative := false
|
|
||||||
pattern := rawpattern
|
|
||||||
if strings.HasPrefix(rawpattern, "!") {
|
|
||||||
negative = true
|
|
||||||
pattern = rawpattern[1:]
|
|
||||||
}
|
|
||||||
rpattern, err := PatternToRegex(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
regex, err := regexp.Compile(rpattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &WorkflowPattern{
|
|
||||||
Pattern: pattern,
|
|
||||||
Negative: negative,
|
|
||||||
Regex: regex,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PatternToRegex(pattern string) (string, error) {
|
|
||||||
var rpattern strings.Builder
|
|
||||||
rpattern.WriteString("^")
|
|
||||||
pos := 0
|
|
||||||
errors := map[int]string{}
|
|
||||||
for pos < len(pattern) {
|
|
||||||
switch pattern[pos] {
|
|
||||||
case '*':
|
|
||||||
if pos+1 < len(pattern) && pattern[pos+1] == '*' {
|
|
||||||
if pos+2 < len(pattern) && pattern[pos+2] == '/' {
|
|
||||||
rpattern.WriteString("(.+/)?")
|
|
||||||
pos += 3
|
|
||||||
} else {
|
|
||||||
rpattern.WriteString(".*")
|
|
||||||
pos += 2
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rpattern.WriteString("[^/]*")
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
case '+', '?':
|
|
||||||
if pos > 0 {
|
|
||||||
rpattern.WriteByte(pattern[pos])
|
|
||||||
} else {
|
|
||||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
|
|
||||||
}
|
|
||||||
pos++
|
|
||||||
case '[':
|
|
||||||
rpattern.WriteByte(pattern[pos])
|
|
||||||
pos++
|
|
||||||
if pos < len(pattern) && pattern[pos] == ']' {
|
|
||||||
errors[pos] = "Unexpected empty brackets '[]'"
|
|
||||||
pos++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
validChar := func(a, b, test byte) bool {
|
|
||||||
return test >= a && test <= b
|
|
||||||
}
|
|
||||||
startPos := pos
|
|
||||||
for pos < len(pattern) && pattern[pos] != ']' {
|
|
||||||
switch pattern[pos] {
|
|
||||||
case '-':
|
|
||||||
if pos <= startPos || pos+1 >= len(pattern) {
|
|
||||||
errors[pos] = "Invalid range"
|
|
||||||
pos++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
validRange := func(a, b byte) bool {
|
|
||||||
return validChar(a, b, pattern[pos-1]) && validChar(a, b, pattern[pos+1]) && pattern[pos-1] <= pattern[pos+1]
|
|
||||||
}
|
|
||||||
if !validRange('A', 'z') && !validRange('0', '9') {
|
|
||||||
errors[pos] = "Ranges can only include a-z, A-Z, A-z, and 0-9"
|
|
||||||
pos++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
rpattern.WriteString(pattern[pos : pos+2])
|
|
||||||
pos += 2
|
|
||||||
default:
|
|
||||||
if !validChar('A', 'z', pattern[pos]) && !validChar('0', '9', pattern[pos]) {
|
|
||||||
errors[pos] = "Ranges can only include a-z, A-Z and 0-9"
|
|
||||||
pos++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pos >= len(pattern) || pattern[pos] != ']' {
|
|
||||||
errors[pos] = "Missing closing bracket ']' after '['"
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
rpattern.WriteString("]")
|
|
||||||
pos++
|
|
||||||
case '\\':
|
|
||||||
if pos+1 >= len(pattern) {
|
|
||||||
errors[pos] = "Missing symbol after \\"
|
|
||||||
pos++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos+1]})))
|
|
||||||
pos += 2
|
|
||||||
default:
|
|
||||||
rpattern.WriteString(regexp.QuoteMeta(string([]byte{pattern[pos]})))
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(errors) > 0 {
|
|
||||||
var errorMessage strings.Builder
|
|
||||||
for position, err := range errors {
|
|
||||||
if errorMessage.Len() > 0 {
|
|
||||||
errorMessage.WriteString(", ")
|
|
||||||
}
|
|
||||||
fmt.Fprintf(&errorMessage, "Position: %d Error: %s", position, err)
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("invalid Pattern '%s': %s", pattern, errorMessage.String())
|
|
||||||
}
|
|
||||||
rpattern.WriteString("$")
|
|
||||||
return rpattern.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) {
|
|
||||||
ret := []*WorkflowPattern{}
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
cp, err := CompilePattern(pattern)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret = append(ret, cp)
|
|
||||||
}
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if the workflow should be skipped paths/branches
|
|
||||||
func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
|
|
||||||
if len(sequence) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, file := range input {
|
|
||||||
matched := false
|
|
||||||
for _, item := range sequence {
|
|
||||||
if item.Regex.MatchString(file) {
|
|
||||||
pattern := item.Pattern
|
|
||||||
if item.Negative {
|
|
||||||
matched = false
|
|
||||||
traceWriter.Info("%s excluded by pattern %s", file, pattern)
|
|
||||||
} else {
|
|
||||||
matched = true
|
|
||||||
traceWriter.Info("%s included by pattern %s", file, pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if matched {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if the workflow should be skipped paths-ignore/branches-ignore
|
|
||||||
func Filter(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool {
|
|
||||||
if len(sequence) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, file := range input {
|
|
||||||
matched := false
|
|
||||||
for _, item := range sequence {
|
|
||||||
if item.Regex.MatchString(file) == !item.Negative {
|
|
||||||
pattern := item.Pattern
|
|
||||||
traceWriter.Info("%s ignored by pattern %s", file, pattern)
|
|
||||||
matched = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matched {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@@ -1,418 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// Copyright 2023 The nektos/act Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package workflowpattern
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMatchPattern(t *testing.T) {
|
|
||||||
kases := []struct {
|
|
||||||
inputs []string
|
|
||||||
patterns []string
|
|
||||||
skipResult bool
|
|
||||||
filterResult bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
patterns: []string{"*"},
|
|
||||||
inputs: []string{"path/with/slash"},
|
|
||||||
skipResult: true,
|
|
||||||
filterResult: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"path/a", "path/b", "path/c"},
|
|
||||||
inputs: []string{"meta", "path/b", "otherfile"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"path/a", "path/b", "path/c"},
|
|
||||||
inputs: []string{"path/b"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"path/a", "path/b", "path/c"},
|
|
||||||
inputs: []string{"path/c", "path/b"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"path/a", "path/b", "path/c"},
|
|
||||||
inputs: []string{"path/c", "path/b", "path/a"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"path/a", "path/b", "path/c"},
|
|
||||||
inputs: []string{"path/c", "path/b", "path/d", "path/a"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{},
|
|
||||||
inputs: []string{},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"\\!file"},
|
|
||||||
inputs: []string{"!file"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"escape\\\\backslash"},
|
|
||||||
inputs: []string{"escape\\backslash"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{".yml"},
|
|
||||||
inputs: []string{"fyml"},
|
|
||||||
skipResult: true,
|
|
||||||
filterResult: false,
|
|
||||||
},
|
|
||||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-branches-and-tags
|
|
||||||
{
|
|
||||||
patterns: []string{"feature/*"},
|
|
||||||
inputs: []string{"feature/my-branch"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"feature/*"},
|
|
||||||
inputs: []string{"feature/your-branch"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"feature/**"},
|
|
||||||
inputs: []string{"feature/beta-a/my-branch"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"feature/**"},
|
|
||||||
inputs: []string{"feature/beta-a/my-branch"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"feature/**"},
|
|
||||||
inputs: []string{"feature/mona/the/octocat"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"main", "releases/mona-the-octocat"},
|
|
||||||
inputs: []string{"main"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"main", "releases/mona-the-octocat"},
|
|
||||||
inputs: []string{"releases/mona-the-octocat"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*"},
|
|
||||||
inputs: []string{"main"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*"},
|
|
||||||
inputs: []string{"releases"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**"},
|
|
||||||
inputs: []string{"all/the/branches"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**"},
|
|
||||||
inputs: []string{"every/tag"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*feature"},
|
|
||||||
inputs: []string{"mona-feature"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*feature"},
|
|
||||||
inputs: []string{"feature"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*feature"},
|
|
||||||
inputs: []string{"ver-10-feature"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"v2*"},
|
|
||||||
inputs: []string{"v2"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"v2*"},
|
|
||||||
inputs: []string{"v2.0"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"v2*"},
|
|
||||||
inputs: []string{"v2.9"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
|
||||||
inputs: []string{"v1.10.1"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"v[12].[0-9]+.[0-9]+"},
|
|
||||||
inputs: []string{"v2.0.0"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#patterns-to-match-file-paths
|
|
||||||
{
|
|
||||||
patterns: []string{"*"},
|
|
||||||
inputs: []string{"README.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*"},
|
|
||||||
inputs: []string{"server.rb"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.jsx?"},
|
|
||||||
inputs: []string{"page.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.jsx?"},
|
|
||||||
inputs: []string{"page.jsx"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**"},
|
|
||||||
inputs: []string{"all/the/files.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.js"},
|
|
||||||
inputs: []string{"app.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.js"},
|
|
||||||
inputs: []string{"index.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**.js"},
|
|
||||||
inputs: []string{"index.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**.js"},
|
|
||||||
inputs: []string{"js/index.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**.js"},
|
|
||||||
inputs: []string{"src/js/app.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"docs/*"},
|
|
||||||
inputs: []string{"docs/README.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"docs/*"},
|
|
||||||
inputs: []string{"docs/file.txt"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"docs/**"},
|
|
||||||
inputs: []string{"docs/README.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"docs/**"},
|
|
||||||
inputs: []string{"docs/mona/octocat.txt"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"docs/**/*.md"},
|
|
||||||
inputs: []string{"docs/README.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"docs/**/*.md"},
|
|
||||||
inputs: []string{"docs/mona/hello-world.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"docs/**/*.md"},
|
|
||||||
inputs: []string{"docs/a/markdown/file.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/docs/**"},
|
|
||||||
inputs: []string{"docs/hello.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/docs/**"},
|
|
||||||
inputs: []string{"dir/docs/my-file.txt"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/docs/**"},
|
|
||||||
inputs: []string{"space/docs/plan/space.doc"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/README.md"},
|
|
||||||
inputs: []string{"README.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/README.md"},
|
|
||||||
inputs: []string{"js/README.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/*src/**"},
|
|
||||||
inputs: []string{"a/src/app.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/*src/**"},
|
|
||||||
inputs: []string{"my-src/code/js/app.js"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/*-post.md"},
|
|
||||||
inputs: []string{"my-post.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/*-post.md"},
|
|
||||||
inputs: []string{"path/their-post.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/migrate-*.sql"},
|
|
||||||
inputs: []string{"migrate-10909.sql"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/migrate-*.sql"},
|
|
||||||
inputs: []string{"db/migrate-v1.0.sql"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"**/migrate-*.sql"},
|
|
||||||
inputs: []string{"db/sept/migrate-v1.sql"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.md", "!README.md"},
|
|
||||||
inputs: []string{"hello.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.md", "!README.md"},
|
|
||||||
inputs: []string{"README.md"},
|
|
||||||
skipResult: true,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.md", "!README.md"},
|
|
||||||
inputs: []string{"docs/hello.md"},
|
|
||||||
skipResult: true,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.md", "!README.md", "README*"},
|
|
||||||
inputs: []string{"hello.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.md", "!README.md", "README*"},
|
|
||||||
inputs: []string{"README.md"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: []string{"*.md", "!README.md", "README*"},
|
|
||||||
inputs: []string{"README.doc"},
|
|
||||||
skipResult: false,
|
|
||||||
filterResult: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, kase := range kases {
|
|
||||||
t.Run(strings.Join(kase.patterns, ","), func(t *testing.T) {
|
|
||||||
patterns, err := CompilePatterns(kase.patterns...)
|
|
||||||
assert.NoError(t, err) //nolint:testifylint // pre-existing issue from nektos/act
|
|
||||||
|
|
||||||
assert.EqualValues(t, kase.skipResult, Skip(patterns, kase.inputs, &StdOutTraceWriter{}), "skipResult") //nolint:testifylint // pre-existing issue from nektos/act
|
|
||||||
assert.EqualValues(t, kase.filterResult, Filter(patterns, kase.inputs, &StdOutTraceWriter{}), "filterResult") //nolint:testifylint // pre-existing issue from nektos/act
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,14 +40,21 @@
|
|||||||
|
|
||||||
### Running `gitea-runner` using Docker-in-Docker (DIND)
|
### Running `gitea-runner` using Docker-in-Docker (DIND)
|
||||||
|
|
||||||
|
- `privileged` has to be set to `true` because in-container Docker daemon requires a lot of kernel capabilities and file system mounts like `procfs` and `sysfs`
|
||||||
|
- `security_opt` sets the `apparmor` profile to `rootlesskit` for hosts running AppArmor (e.g. Ubuntu, Debian), where the kernel might otherwise block user namespace changes that Docker daemon requires for startup. The `rootlesskit` profile is provided by the `docker-ce-rootless-extras` package and is present on hosts where Docker was installed via the official installer or distro packages
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
...
|
...
|
||||||
runner:
|
runner:
|
||||||
image: gitea/runner:latest-dind-rootless
|
image: gitea/runner:latest-dind-rootless
|
||||||
restart: always
|
restart: always
|
||||||
privileged: true
|
privileged: true
|
||||||
|
security_opt:
|
||||||
|
- apparmor=rootlesskit
|
||||||
depends_on:
|
depends_on:
|
||||||
- gitea
|
gitea:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: true
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/runner:/data
|
- ./data/runner:/data
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.
|
NOTE: Docker in Docker (dind) requires elevated privileges on Kubernetes. The current way to achieve this is to set the pod `SecurityContext` to `privileged`. Keep in mind that this is a potential security issue that has the potential for a malicious application to break out of the container context.
|
||||||
|
|
||||||
|
NOTE: `dind-docker.yaml` uses the native sidecar pattern (init container with `restartPolicy: Always`), which requires Kubernetes 1.29+ (or 1.28 with the `SidecarContainers` feature gate).
|
||||||
|
|
||||||
|
NOTE: A helm chart for `gitea-runner` also exists for easier deployments https://gitea.com/gitea/helm-actions
|
||||||
|
|
||||||
Files in this directory:
|
Files in this directory:
|
||||||
|
|
||||||
- [`dind-docker.yaml`](dind-docker.yaml)
|
- [`dind-docker.yaml`](dind-docker.yaml)
|
||||||
|
|||||||
@@ -35,28 +35,35 @@ spec:
|
|||||||
strategy: {}
|
strategy: {}
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
|
||||||
labels:
|
labels:
|
||||||
app: runner
|
app: runner
|
||||||
spec:
|
spec:
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
volumes:
|
volumes:
|
||||||
- name: docker-certs
|
- name: docker-socket
|
||||||
emptyDir: {}
|
emptyDir: {}
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: runner-vol
|
claimName: runner-vol
|
||||||
|
initContainers:
|
||||||
|
- name: docker
|
||||||
|
image: docker:28.2.2-dind
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
volumeMounts:
|
||||||
|
- name: docker-socket
|
||||||
|
mountPath: /var/run
|
||||||
|
startupProbe:
|
||||||
|
exec:
|
||||||
|
command: ["/usr/bin/test", "-S", "/var/run/docker.sock"]
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["/usr/bin/test", "-S", "/var/run/docker.sock"]
|
||||||
|
restartPolicy: Always
|
||||||
containers:
|
containers:
|
||||||
- name: runner
|
- name: runner
|
||||||
image: gitea/runner:nightly
|
image: gitea/runner:nightly
|
||||||
command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
|
|
||||||
env:
|
env:
|
||||||
- name: DOCKER_HOST
|
|
||||||
value: tcp://localhost:2376
|
|
||||||
- name: DOCKER_CERT_PATH
|
|
||||||
value: /certs/client
|
|
||||||
- name: DOCKER_TLS_VERIFY
|
|
||||||
value: "1"
|
|
||||||
- name: GITEA_INSTANCE_URL
|
- name: GITEA_INSTANCE_URL
|
||||||
value: http://gitea-http.gitea.svc.cluster.local:3000
|
value: http://gitea-http.gitea.svc.cluster.local:3000
|
||||||
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
- name: GITEA_RUNNER_REGISTRATION_TOKEN
|
||||||
@@ -65,17 +72,7 @@ spec:
|
|||||||
name: runner-secret
|
name: runner-secret
|
||||||
key: token
|
key: token
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: docker-certs
|
|
||||||
mountPath: /certs
|
|
||||||
- name: runner-data
|
- name: runner-data
|
||||||
mountPath: /data
|
mountPath: /data
|
||||||
- name: daemon
|
- name: docker-socket
|
||||||
image: docker:23.0.6-dind
|
mountPath: /var/run
|
||||||
env:
|
|
||||||
- name: DOCKER_TLS_CERTDIR
|
|
||||||
value: /certs
|
|
||||||
securityContext:
|
|
||||||
privileged: true
|
|
||||||
volumeMounts:
|
|
||||||
- name: docker-certs
|
|
||||||
mountPath: /certs
|
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ spec:
|
|||||||
strategy: {}
|
strategy: {}
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
creationTimestamp: null
|
|
||||||
labels:
|
labels:
|
||||||
app: runner
|
app: runner
|
||||||
spec:
|
spec:
|
||||||
@@ -50,7 +49,6 @@ spec:
|
|||||||
- name: runner
|
- name: runner
|
||||||
image: gitea/runner:nightly-dind-rootless
|
image: gitea/runner:nightly-dind-rootless
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
# command: ["sh", "-c", "while ! nc -z localhost 2376 </dev/null; do echo 'waiting for docker daemon...'; sleep 5; done; /sbin/tini -- run.sh"]
|
|
||||||
env:
|
env:
|
||||||
- name: DOCKER_HOST
|
- name: DOCKER_HOST
|
||||||
value: tcp://localhost:2376
|
value: tcp://localhost:2376
|
||||||
|
|||||||
78
go.mod
78
go.mod
@@ -4,50 +4,47 @@ go 1.26.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
code.gitea.io/actions-proto-go v0.4.1
|
code.gitea.io/actions-proto-go v0.4.1
|
||||||
connectrpc.com/connect v1.19.2
|
connectrpc.com/connect v1.20.0
|
||||||
github.com/avast/retry-go/v4 v4.7.0
|
dario.cat/mergo v1.0.2
|
||||||
github.com/docker/docker v25.0.15+incompatible
|
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
github.com/mattn/go-isatty v0.0.22
|
|
||||||
github.com/sirupsen/logrus v1.9.4
|
|
||||||
github.com/spf13/cobra v1.10.2
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
|
||||||
golang.org/x/term v0.42.0
|
|
||||||
golang.org/x/time v0.14.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.11
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gotest.tools/v3 v3.5.2
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
|
github.com/avast/retry-go/v5 v5.0.0
|
||||||
|
github.com/containerd/errdefs v1.0.0
|
||||||
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 v25.0.7+incompatible
|
github.com/docker/cli v29.5.2+incompatible
|
||||||
github.com/docker/go-connections v0.6.0
|
github.com/docker/go-connections v0.7.0
|
||||||
github.com/go-git/go-billy/v5 v5.9.0
|
github.com/go-git/go-billy/v5 v5.9.0
|
||||||
github.com/go-git/go-git/v5 v5.19.0
|
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/imdario/mergo v0.3.16
|
github.com/google/go-cmp v0.7.0
|
||||||
|
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/moby/buildkit v0.13.2
|
github.com/mattn/go-isatty v0.0.22
|
||||||
|
github.com/moby/go-archive v0.2.0
|
||||||
|
github.com/moby/moby/api v1.54.2
|
||||||
|
github.com/moby/moby/client v0.4.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.14.0
|
github.com/opencontainers/selinux v1.15.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/rhysd/actionlint v1.7.12
|
github.com/rhysd/actionlint v1.7.12
|
||||||
|
github.com/sirupsen/logrus v1.9.4
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
github.com/timshannon/bolthold v0.0.0-20240314194003-30aac6950928
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
|
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||||
|
golang.org/x/term v0.43.0
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
gotest.tools/v3 v3.5.2
|
||||||
|
tags.cncf.io/container-device-interface v1.1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cyphar.com/go-pathrs v0.2.3 // indirect
|
cyphar.com/go-pathrs v0.2.3 // indirect
|
||||||
dario.cat/mergo v1.0.2 // indirect
|
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
@@ -55,11 +52,11 @@ require (
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/containerd/containerd v1.7.29 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
github.com/docker/docker-credential-helpers v0.9.6 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fatih/color v1.19.0 // indirect
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
@@ -67,30 +64,28 @@ require (
|
|||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||||
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.18.4 // indirect
|
github.com/klauspost/compress v1.18.5 // 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/mitchellh/mapstructure v1.1.2 // 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.6.0 // indirect
|
||||||
github.com/moby/sys/user v0.4.0 // indirect
|
github.com/moby/sys/user v0.4.0 // indirect
|
||||||
github.com/moby/sys/userns v0.1.0 // indirect
|
github.com/moby/sys/userns v0.1.0 // indirect
|
||||||
github.com/moby/term v0.5.2 // 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/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.66.1 // indirect
|
github.com/prometheus/common v0.66.1 // indirect
|
||||||
github.com/prometheus/procfs v0.16.1 // indirect
|
github.com/prometheus/procfs v0.17.0 // 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
|
||||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||||
@@ -101,16 +96,17 @@ require (
|
|||||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/net v0.53.0 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.43.0 // indirect
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
|
||||||
google.golang.org/grpc v1.67.0 // indirect
|
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
204
go.sum
204
go.sum
@@ -2,43 +2,41 @@ code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLr
|
|||||||
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
|
||||||
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
|
||||||
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
|
||||||
|
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||||
|
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||||
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
cyphar.com/go-pathrs v0.2.3 h1:0pH8gep37wB0BgaXrEaN1OtZhUMeS7VvaejSr6i822o=
|
||||||
cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
cyphar.com/go-pathrs v0.2.3/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=
|
||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/Microsoft/hcsshim v0.11.7 h1:vl/nj3Bar/CvJSYo7gIQPyRWc9f3c6IeSNavBTSZNZQ=
|
|
||||||
github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
|
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
|
github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k=
|
||||||
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
|
github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/containerd/containerd v1.7.29 h1:90fWABQsaN9mJhGkoVnuzEY+o1XDPbg9BTC9QTAHnuE=
|
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||||
github.com/containerd/containerd v1.7.29/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
@@ -51,26 +49,18 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/cli v25.0.7+incompatible h1:scW/AbGafKmANsonsFckFHTwpz2QypoPA/zpoLnDs/E=
|
github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ=
|
||||||
github.com/docker/cli v25.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/docker v25.0.13+incompatible h1:YeBrkUd3q0ZoRDNoEzuopwCLU+uD8GZahDHwBdsTnkU=
|
github.com/docker/docker-credential-helpers v0.9.6 h1:cT2PbRPSlnMmNTfT2TDMXRyQ1KMWHG7xoTLBcn1ZNv0=
|
||||||
github.com/docker/docker v25.0.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker-credential-helpers v0.9.6/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||||
github.com/docker/docker v25.0.14+incompatible h1:+HNue3fKbqiDHYFAriyiMjfS5u25zB0E2/R8f42lOMc=
|
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||||
github.com/docker/docker v25.0.14+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||||
github.com/docker/docker v25.0.15+incompatible h1:JhRD6vZdk0Ms3SEMztefBISJL13NbxudQnGix6l+T5M=
|
|
||||||
github.com/docker/docker v25.0.15+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
|
||||||
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
|
||||||
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
|
||||||
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=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
|
||||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
@@ -79,25 +69,21 @@ 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.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
|
||||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
|
||||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||||
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.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||||
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.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||||
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||||
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
|
|
||||||
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
@@ -106,10 +92,6 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
|
|||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
|
||||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
|
||||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
@@ -122,10 +104,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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
|
||||||
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=
|
||||||
@@ -139,22 +119,20 @@ 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
|
||||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
|
||||||
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/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/buildkit v0.13.2 h1:nXNszM4qD9E7QtG7bFWPnDI1teUQFQglBzon/IU3SzI=
|
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||||
github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY=
|
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
|
||||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||||
|
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||||
|
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||||
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.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||||
@@ -163,10 +141,6 @@ github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
|||||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||||
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/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||||
@@ -175,12 +149,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=
|
github.com/opencontainers/selinux v1.14.1 h1:a7XlXV/nN/l5zFP1FWZYoExpClu1QOPMfWUV2CZ8kEQ=
|
||||||
github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=
|
github.com/opencontainers/selinux v1.14.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||||
github.com/opencontainers/selinux v1.14.0 h1:k1w6YWg3w/TvfZUAc3ksdaRwGNulRbE88TxqAZxUSOE=
|
github.com/opencontainers/selinux v1.15.0 h1:4Gs40e/R2FvM8PC1HPaPncLLaDor8Y2WDfk5gjU9o5M=
|
||||||
github.com/opencontainers/selinux v1.14.0/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
github.com/opencontainers/selinux v1.15.0/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
|
||||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -193,10 +165,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
|||||||
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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||||
github.com/rhysd/actionlint v1.7.11 h1:m+aSuCpCIClS8X02xMG4Z8s87fCHPsAtYkAoWGQZgEE=
|
|
||||||
github.com/rhysd/actionlint v1.7.11/go.mod h1:8n50YougV9+50niD7oxgDTZ1KbN/ZnKiQ2xpLFeVhsI=
|
|
||||||
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
|
github.com/rhysd/actionlint v1.7.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY=
|
||||||
github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g=
|
github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
@@ -240,8 +210,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
|
|||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
|
||||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
@@ -250,108 +218,51 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
|||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
|
||||||
golang.org/x/crypto v0.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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
|
||||||
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
|
|
||||||
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -361,10 +272,13 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
|
tags.cncf.io/container-device-interface v1.1.0 h1:RnxNhxF1JOu6CJUVpetTYvrXHdxw9j9jFYgZpI+anSY=
|
||||||
|
tags.cncf.io/container-device-interface v1.1.0/go.mod h1:76Oj0Yqp9FwTx/pySDc8Bxjpg+VqXfDb50cKAXVJ34Q=
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
|
|||||||
cfg.Runner.Insecure,
|
cfg.Runner.Insecure,
|
||||||
reg.UUID,
|
reg.UUID,
|
||||||
reg.Token,
|
reg.Token,
|
||||||
ver.Version(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
runner := run.NewRunner(cfg, reg, cli)
|
runner := run.NewRunner(cfg, reg, cli)
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import (
|
|||||||
"gitea.com/gitea/runner/act/model"
|
"gitea.com/gitea/runner/act/model"
|
||||||
"gitea.com/gitea/runner/act/runner"
|
"gitea.com/gitea/runner/act/runner"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types/container"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/moby/moby/api/types/container"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|||||||
@@ -325,7 +325,6 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
|||||||
cfg.Runner.Insecure,
|
cfg.Runner.Insecure,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
ver.Version(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -369,7 +368,6 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
|
|||||||
Name: reg.Name,
|
Name: reg.Name,
|
||||||
Token: reg.Token,
|
Token: reg.Token,
|
||||||
Version: ver.Version(),
|
Version: ver.Version(),
|
||||||
AgentLabels: ls, // Could be removed after Gitea 1.20
|
|
||||||
Labels: ls,
|
Labels: ls,
|
||||||
Ephemeral: reg.Ephemeral,
|
Ephemeral: reg.Ephemeral,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import (
|
|||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/moby/moby/api/types/container"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -218,6 +218,14 @@ func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Runner) cloneEnvs() map[string]string {
|
||||||
|
// +3 reserves space for the per-task keys injected by run():
|
||||||
|
// ACTIONS_ID_TOKEN_REQUEST_URL, ACTIONS_ID_TOKEN_REQUEST_TOKEN, ACTIONS_RUNTIME_TOKEN.
|
||||||
|
envs := make(map[string]string, len(r.envs)+3)
|
||||||
|
maps.Copy(envs, r.envs)
|
||||||
|
return envs
|
||||||
|
}
|
||||||
|
|
||||||
// getDefaultActionsURL
|
// getDefaultActionsURL
|
||||||
// when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank,
|
// when DEFAULT_ACTIONS_URL == "https://github.com" and GithubMirror is not blank,
|
||||||
// it should be set to GithubMirror first.
|
// it should be set to GithubMirror first.
|
||||||
@@ -251,6 +259,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
reporter.ResetSteps(len(job.Steps))
|
reporter.ResetSteps(len(job.Steps))
|
||||||
|
|
||||||
taskContext := task.Context.Fields
|
taskContext := task.Context.Fields
|
||||||
|
envs := r.cloneEnvs()
|
||||||
|
|
||||||
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
log.Infof("task %v repo is %v %v %v", task.Id, taskContext["repository"].GetStringValue(),
|
||||||
r.getDefaultActionsURL(task),
|
r.getDefaultActionsURL(task),
|
||||||
@@ -281,9 +290,9 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" {
|
if actionsIDTokenRequestURL := taskContext["actions_id_token_request_url"].GetStringValue(); actionsIDTokenRequestURL != "" {
|
||||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL
|
envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = actionsIDTokenRequestURL
|
||||||
r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue()
|
envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = taskContext["actions_id_token_request_token"].GetStringValue()
|
||||||
task.Secrets["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = r.envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
|
task.Secrets["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
|
||||||
}
|
}
|
||||||
|
|
||||||
giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue()
|
giteaRuntimeToken := taskContext["gitea_runtime_token"].GetStringValue()
|
||||||
@@ -291,7 +300,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
// use task token to action api token for previous Gitea Server Versions
|
// use task token to action api token for previous Gitea Server Versions
|
||||||
giteaRuntimeToken = preset.Token
|
giteaRuntimeToken = preset.Token
|
||||||
}
|
}
|
||||||
r.envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
|
envs["ACTIONS_RUNTIME_TOKEN"] = giteaRuntimeToken
|
||||||
// Mask the runtime token so it cannot be echoed in user step output; it is
|
// Mask the runtime token so it cannot be echoed in user step output; it is
|
||||||
// now also the cache server's bearer credential and leaking it would let
|
// now also the cache server's bearer credential and leaking it would let
|
||||||
// any reader of the log impersonate this job against the cache.
|
// any reader of the log impersonate this job against the cache.
|
||||||
@@ -338,13 +347,15 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
Workdir: workdir,
|
Workdir: workdir,
|
||||||
BindWorkdir: r.cfg.Container.BindWorkdir,
|
BindWorkdir: r.cfg.Container.BindWorkdir,
|
||||||
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
ActionCacheDir: filepath.FromSlash(r.cfg.Host.WorkdirParent),
|
||||||
|
AllocatePTY: r.cfg.Runner.AllocatePTY,
|
||||||
|
ActionOfflineMode: r.cfg.Cache.OfflineMode,
|
||||||
|
|
||||||
ReuseContainers: false,
|
ReuseContainers: false,
|
||||||
ForcePull: r.cfg.Container.ForcePull,
|
ForcePull: r.cfg.Container.ForcePull,
|
||||||
ForceRebuild: r.cfg.Container.ForceRebuild,
|
ForceRebuild: r.cfg.Container.ForceRebuild,
|
||||||
LogOutput: true,
|
LogOutput: true,
|
||||||
JSONLogger: false,
|
JSONLogger: false,
|
||||||
Env: r.envs,
|
Env: envs,
|
||||||
Secrets: task.Secrets,
|
Secrets: task.Secrets,
|
||||||
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
GitHubInstance: strings.TrimSuffix(r.client.Address(), "/"),
|
||||||
AutoRemove: true,
|
AutoRemove: true,
|
||||||
@@ -353,6 +364,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
EventJSON: string(eventJSON),
|
EventJSON: string(eventJSON),
|
||||||
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
ContainerNamePrefix: fmt.Sprintf("GITEA-ACTIONS-TASK-%d", task.Id),
|
||||||
ContainerMaxLifetime: maxLifetime,
|
ContainerMaxLifetime: maxLifetime,
|
||||||
|
CleanWorkdir: true,
|
||||||
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
ContainerNetworkMode: container.NetworkMode(r.cfg.Container.Network),
|
||||||
ContainerOptions: r.cfg.Container.Options,
|
ContainerOptions: r.cfg.Container.Options,
|
||||||
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
ContainerDaemonSocket: r.cfg.Container.DockerHost,
|
||||||
|
|||||||
61
internal/app/run/runner_env_test.go
Normal file
61
internal/app/run/runner_env_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package run
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunnerCloneEnvsReturnsTaskLocalCopy(t *testing.T) {
|
||||||
|
r := &Runner{
|
||||||
|
envs: map[string]string{
|
||||||
|
"ACTIONS_CACHE_URL": "http://cache.example",
|
||||||
|
"ACTIONS_RUNTIME_URL": "http://runner.example",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cloned := r.cloneEnvs()
|
||||||
|
require.Equal(t, r.envs, cloned)
|
||||||
|
|
||||||
|
cloned["ACTIONS_RUNTIME_TOKEN"] = "task-token"
|
||||||
|
cloned["ACTIONS_ID_TOKEN_REQUEST_URL"] = "http://oidc.example"
|
||||||
|
|
||||||
|
assert.NotContains(t, r.envs, "ACTIONS_RUNTIME_TOKEN")
|
||||||
|
assert.NotContains(t, r.envs, "ACTIONS_ID_TOKEN_REQUEST_URL")
|
||||||
|
assert.Equal(t, "http://cache.example", r.envs["ACTIONS_CACHE_URL"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression test for #958: concurrent tasks writing task-specific env keys
|
||||||
|
// used to race on the shared r.envs map and crash the runner with
|
||||||
|
// "fatal error: concurrent map writes". Each task must mutate its own clone.
|
||||||
|
func TestRunnerCloneEnvsConcurrentMutation(t *testing.T) {
|
||||||
|
r := &Runner{
|
||||||
|
envs: map[string]string{
|
||||||
|
"ACTIONS_CACHE_URL": "http://cache.example",
|
||||||
|
"ACTIONS_RUNTIME_URL": "http://runner.example",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const goroutines = 16
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(goroutines)
|
||||||
|
for range goroutines {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
envs := r.cloneEnvs()
|
||||||
|
envs["ACTIONS_RUNTIME_TOKEN"] = "task-token"
|
||||||
|
envs["ACTIONS_ID_TOKEN_REQUEST_URL"] = "http://oidc.example"
|
||||||
|
envs["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = "oidc-token"
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.NotContains(t, r.envs, "ACTIONS_RUNTIME_TOKEN")
|
||||||
|
assert.NotContains(t, r.envs, "ACTIONS_ID_TOKEN_REQUEST_URL")
|
||||||
|
assert.NotContains(t, r.envs, "ACTIONS_ID_TOKEN_REQUEST_TOKEN")
|
||||||
|
}
|
||||||
@@ -6,6 +6,4 @@ package client
|
|||||||
const (
|
const (
|
||||||
UUIDHeader = "x-runner-uuid"
|
UUIDHeader = "x-runner-uuid"
|
||||||
TokenHeader = "x-runner-token"
|
TokenHeader = "x-runner-token"
|
||||||
// Deprecated: could be removed after Gitea 1.20 released
|
|
||||||
VersionHeader = "x-runner-version"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
MaxIdleConnsPerHost: 10, // All requests go to one host; default is 2 which causes frequent reconnects.
|
MaxIdleConnsPerHost: 10, // All requests go to one host; default is 2 which causes frequent reconnects.
|
||||||
IdleConnTimeout: 90 * time.Second,
|
IdleConnTimeout: 90 * time.Second,
|
||||||
@@ -30,7 +31,7 @@ func getHTTPClient(endpoint string, insecure bool) *http.Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new runner client.
|
// New returns a new runner client.
|
||||||
func New(endpoint string, insecure bool, uuid, token, version string, opts ...connect.ClientOption) *HTTPClient {
|
func New(endpoint string, insecure bool, uuid, token string, opts ...connect.ClientOption) *HTTPClient {
|
||||||
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
|
||||||
|
|
||||||
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||||
@@ -41,10 +42,6 @@ func New(endpoint string, insecure bool, uuid, token, version string, opts ...co
|
|||||||
if token != "" {
|
if token != "" {
|
||||||
req.Header().Set(TokenHeader, token)
|
req.Header().Set(TokenHeader, token)
|
||||||
}
|
}
|
||||||
// TODO: version will be removed from request header after Gitea 1.20 released.
|
|
||||||
if version != "" {
|
|
||||||
req.Header().Set(VersionHeader, version)
|
|
||||||
}
|
|
||||||
return next(ctx, req)
|
return next(ctx, req)
|
||||||
}
|
}
|
||||||
})))
|
})))
|
||||||
|
|||||||
27
internal/pkg/client/http_test.go
Normal file
27
internal/pkg/client/http_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetHTTPClientUsesProxyFromEnvironment(t *testing.T) {
|
||||||
|
t.Setenv("HTTP_PROXY", "http://proxy.example.com:8080")
|
||||||
|
|
||||||
|
client := getHTTPClient("http://gitea.example.com", false)
|
||||||
|
transport, ok := client.Transport.(*http.Transport)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "http://gitea.example.com/api/actions/ping", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxyURL, err := transport.Proxy(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, proxyURL)
|
||||||
|
require.Equal(t, "http://proxy.example.com:8080", proxyURL.String())
|
||||||
|
}
|
||||||
@@ -74,6 +74,11 @@ runner:
|
|||||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||||
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
- "ubuntu-24.04:docker://docker.gitea.com/runner-images:ubuntu-24.04"
|
||||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||||
|
# Allocate a pseudo-TTY for each step's process. Applies to both host and docker backends.
|
||||||
|
# Default false matches GitHub actions/runner. Enable only for jobs that need an interactive
|
||||||
|
# terminal; tools like `docker build` emit redrawing progress frames into the captured log
|
||||||
|
# when a TTY is present.
|
||||||
|
allocate_pty: false
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
# Enable cache server to use actions/cache.
|
# Enable cache server to use actions/cache.
|
||||||
@@ -97,6 +102,9 @@ cache:
|
|||||||
# (or `gitea-runner cache-server`) is in use: the runner pre-registers each job's ACTIONS_RUNTIME_TOKEN with the
|
# (or `gitea-runner cache-server`) is in use: the runner pre-registers each job's ACTIONS_RUNTIME_TOKEN with the
|
||||||
# cache-server, and the cache-server enforces bearer auth + per-repo cache isolation.
|
# cache-server, and the cache-server enforces bearer auth + per-repo cache isolation.
|
||||||
external_secret: ""
|
external_secret: ""
|
||||||
|
# When true, reuse a cached action instead of fetching from the remote on every job. Note: a moved tag
|
||||||
|
# (e.g. a re-tagged "v6") or an updated branch stays at the cached commit until its cache entry is removed.
|
||||||
|
offline_mode: false
|
||||||
|
|
||||||
container:
|
container:
|
||||||
# Specifies the network to which the container will connect.
|
# Specifies the network to which the container will connect.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Runner struct {
|
|||||||
StateReportInterval time.Duration `yaml:"state_report_interval"` // StateReportInterval specifies the interval for state reporting.
|
StateReportInterval time.Duration `yaml:"state_report_interval"` // StateReportInterval specifies the interval for state reporting.
|
||||||
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup
|
||||||
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
GithubMirror string `yaml:"github_mirror"` // GithubMirror defines what mirrors should be used when using github
|
||||||
|
AllocatePTY bool `yaml:"allocate_pty"` // AllocatePTY allocates a pseudo-TTY for each step's process. Default is false, matching GitHub's actions/runner. Enable only for jobs that need an interactive terminal; tools like docker build emit redrawing progress frames into the captured log when a TTY is present. Applies to both host and docker backends.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache represents the configuration for caching.
|
// Cache represents the configuration for caching.
|
||||||
@@ -51,6 +52,7 @@ type Cache struct {
|
|||||||
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
||||||
ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server
|
ExternalServer string `yaml:"external_server"` // ExternalServer specifies the URL of external cache server
|
||||||
ExternalSecret string `yaml:"external_secret"` // ExternalSecret is a shared secret between this runner and an external gitea-runner cache-server, enabling per-job ACTIONS_RUNTIME_TOKEN authentication and repo scoping over the network. Leave empty to keep the legacy unauthenticated behavior.
|
ExternalSecret string `yaml:"external_secret"` // ExternalSecret is a shared secret between this runner and an external gitea-runner cache-server, enabling per-job ACTIONS_RUNTIME_TOKEN authentication and repo scoping over the network. Leave empty to keep the legacy unauthenticated behavior.
|
||||||
|
OfflineMode bool `yaml:"offline_mode"` // OfflineMode reuses a cached action without fetching from the remote; a moved tag or branch stays at the cached commit until the cache entry is removed.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container represents the configuration for the container.
|
// Container represents the configuration for the container.
|
||||||
@@ -108,7 +110,6 @@ func LoadDefault(file string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("parse config file %q for defaults metadata: %w", file, err)
|
return nil, fmt.Errorf("parse config file %q for defaults metadata: %w", file, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compatibleWithOldEnvs(file != "", cfg)
|
|
||||||
|
|
||||||
if cfg.Runner.EnvFile != "" {
|
if cfg.Runner.EnvFile != "" {
|
||||||
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
if stat, err := os.Stat(cfg.Runner.EnvFile); err == nil && !stat.IsDir() {
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Deprecated: could be removed in the future. TODO: remove it when Gitea 1.20.0 is released.
|
|
||||||
// Be compatible with old envs.
|
|
||||||
func compatibleWithOldEnvs(fileUsed bool, cfg *Config) {
|
|
||||||
handleEnv := func(key string) (string, bool) {
|
|
||||||
if v, ok := os.LookupEnv(key); ok {
|
|
||||||
if fileUsed {
|
|
||||||
log.Warnf("env %s has been ignored because config file is used", key)
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
log.Warnf("env %s will be deprecated, please use config file instead", key)
|
|
||||||
return v, true
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := handleEnv("GITEA_DEBUG"); ok {
|
|
||||||
if b, _ := strconv.ParseBool(v); b {
|
|
||||||
cfg.Log.Level = "debug"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_TRACE"); ok {
|
|
||||||
if b, _ := strconv.ParseBool(v); b {
|
|
||||||
cfg.Log.Level = "trace"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_CAPACITY"); ok {
|
|
||||||
if i, _ := strconv.Atoi(v); i > 0 {
|
|
||||||
cfg.Runner.Capacity = i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_FILE"); ok {
|
|
||||||
cfg.Runner.File = v
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_ENVIRON"); ok {
|
|
||||||
splits := strings.Split(v, ",")
|
|
||||||
if cfg.Runner.Envs == nil {
|
|
||||||
cfg.Runner.Envs = map[string]string{}
|
|
||||||
}
|
|
||||||
for _, split := range splits {
|
|
||||||
kv := strings.SplitN(split, ":", 2)
|
|
||||||
if len(kv) == 2 && kv[0] != "" {
|
|
||||||
cfg.Runner.Envs[kv[0]] = kv[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, ok := handleEnv("GITEA_RUNNER_ENV_FILE"); ok {
|
|
||||||
cfg.Runner.EnvFile = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/moby/moby/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
||||||
@@ -19,13 +19,13 @@ func CheckIfDockerRunning(ctx context.Context, configDockerHost string) error {
|
|||||||
opts = append(opts, client.WithHost(configDockerHost))
|
opts = append(opts, client.WithHost(configDockerHost))
|
||||||
}
|
}
|
||||||
|
|
||||||
cli, err := client.NewClientWithOpts(opts...)
|
cli, err := client.New(opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer cli.Close()
|
defer cli.Close()
|
||||||
|
|
||||||
_, err = cli.Ping(ctx)
|
_, err = cli.Ping(ctx, client.PingOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot ping the docker daemon, is it running? %w", err)
|
return fmt.Errorf("cannot ping the docker daemon, is it running? %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
"github.com/avast/retry-go/v4"
|
"github.com/avast/retry-go/v5"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
@@ -416,14 +416,29 @@ func (r *Reporter) Close(lastWords string) error {
|
|||||||
log.Error("No Response from RunDaemon for 60s, continue best effort")
|
log.Error("No Response from RunDaemon for 60s, continue best effort")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gitea's UpdateLog short-circuits on len(Rows)==0 before honoring NoMore,
|
||||||
|
// so a final empty request never runs TransferLogs and dbfs_data leaks.
|
||||||
|
// Inject a sentinel row after the daemon has exited so it can't be flushed
|
||||||
|
// before ReportLog(true).
|
||||||
|
// TODO: Remove after https://github.com/go-gitea/gitea/pull/37631 is in all
|
||||||
|
// supported branches, e.g. v1.28+.
|
||||||
|
r.stateMu.Lock()
|
||||||
|
if len(r.logRows) == 0 {
|
||||||
|
r.logRows = append(r.logRows, &runnerv1.LogRow{
|
||||||
|
Time: timestamppb.Now(),
|
||||||
|
Content: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
r.stateMu.Unlock()
|
||||||
|
|
||||||
// Report the job outcome even when all log upload retry attempts have been exhausted
|
// Report the job outcome even when all log upload retry attempts have been exhausted
|
||||||
return errors.Join(
|
return errors.Join(
|
||||||
retry.Do(func() error {
|
retry.New(retry.Context(r.ctx)).Do(func() error {
|
||||||
return r.ReportLog(true)
|
return r.ReportLog(true)
|
||||||
}, retry.Context(r.ctx)),
|
}),
|
||||||
retry.Do(func() error {
|
retry.New(retry.Context(r.ctx)).Do(func() error {
|
||||||
return r.ReportState(true)
|
return r.ReportState(true)
|
||||||
}, retry.Context(r.ctx)),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,7 +639,7 @@ func (r *Reporter) handleCommand(originalContent, command, value string) *string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
|
||||||
content := strings.TrimRightFunc(entry.Message, func(r rune) bool { return r == '\r' || r == '\n' })
|
content := strings.TrimRight(entry.Message, "\r\n")
|
||||||
|
|
||||||
matches := cmdRegex.FindStringSubmatch(content)
|
matches := cmdRegex.FindStringSubmatch(content)
|
||||||
if matches != nil {
|
if matches != nil {
|
||||||
|
|||||||
@@ -598,6 +598,68 @@ func TestReporter_StateNotifyFlush(t *testing.T) {
|
|||||||
"step transition should have triggered immediate state flush via stateNotify")
|
"step transition should have triggered immediate state flush via stateNotify")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression test for https://gitea.com/gitea/runner/issues/950: Close() must
|
||||||
|
// always send a final UpdateLog with NoMore=true carrying at least one row,
|
||||||
|
// otherwise the server's len(Rows)==0 short-circuit skips TransferLogs.
|
||||||
|
// TODO: Remove after https://github.com/go-gitea/gitea/pull/37631 is in all
|
||||||
|
// supported branches, e.g. v1.28+.
|
||||||
|
func TestReporter_CloseAlwaysSendsRowsWithNoMore(t *testing.T) {
|
||||||
|
var lastReq atomic.Pointer[runnerv1.UpdateLogRequest]
|
||||||
|
var noMoreCalls atomic.Int64
|
||||||
|
|
||||||
|
client := mocks.NewClient(t)
|
||||||
|
client.On("UpdateLog", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||||
|
lastReq.Store(req.Msg)
|
||||||
|
if req.Msg.NoMore {
|
||||||
|
noMoreCalls.Add(1)
|
||||||
|
}
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateLogResponse{
|
||||||
|
AckIndex: req.Msg.Index + int64(len(req.Msg.Rows)),
|
||||||
|
}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.On("UpdateTask", mock.Anything, mock.Anything).Return(
|
||||||
|
func(_ context.Context, _ *connect_go.Request[runnerv1.UpdateTaskRequest]) (*connect_go.Response[runnerv1.UpdateTaskResponse], error) {
|
||||||
|
return connect_go.NewResponse(&runnerv1.UpdateTaskResponse{}), nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
taskCtx, err := structpb.NewStruct(map[string]any{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Intervals large enough that no daemon-driven flush fires during the test.
|
||||||
|
cfg, _ := config.LoadDefault("")
|
||||||
|
cfg.Runner.LogReportInterval = 10 * time.Second
|
||||||
|
cfg.Runner.LogReportMaxLatency = 10 * time.Second
|
||||||
|
cfg.Runner.StateReportInterval = 10 * time.Second
|
||||||
|
cfg.Runner.LogReportBatchSize = 1000
|
||||||
|
|
||||||
|
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||||
|
reporter.ResetSteps(1)
|
||||||
|
reporter.RunDaemon()
|
||||||
|
|
||||||
|
// Simulate a successful job whose log buffer was already drained by the
|
||||||
|
// daemon (logOffset > 0, logRows empty, terminal Result set). This is the
|
||||||
|
// state Close() lands in for the typical successful job under #819.
|
||||||
|
reporter.stateMu.Lock()
|
||||||
|
reporter.logOffset = 5
|
||||||
|
reporter.state.Result = runnerv1.Result_RESULT_SUCCESS
|
||||||
|
reporter.state.Steps[0].Result = runnerv1.Result_RESULT_SUCCESS
|
||||||
|
reporter.state.StoppedAt = timestamppb.Now()
|
||||||
|
reporter.stateMu.Unlock()
|
||||||
|
|
||||||
|
require.NoError(t, reporter.Close(""))
|
||||||
|
|
||||||
|
require.Equal(t, int64(1), noMoreCalls.Load(), "Close must send exactly one UpdateLog with NoMore=true")
|
||||||
|
final := lastReq.Load()
|
||||||
|
require.NotNil(t, final)
|
||||||
|
assert.True(t, final.NoMore, "final UpdateLog must carry NoMore=true")
|
||||||
|
assert.NotEmpty(t, final.Rows, "final UpdateLog must carry at least one row")
|
||||||
|
}
|
||||||
|
|
||||||
// TestReporter_StateHeartbeat verifies that ReportState sends a heartbeat
|
// TestReporter_StateHeartbeat verifies that ReportState sends a heartbeat
|
||||||
// UpdateTask once stateReportInterval has elapsed since the last successful
|
// UpdateTask once stateReportInterval has elapsed since the last successful
|
||||||
// report, even when nothing has changed. Without this, long-running silent
|
// report, even when nothing has changed. Without this, long-running silent
|
||||||
|
|||||||
19
tools/lint-pr-title.ts
Normal file
19
tools/lint-pr-title.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import {env, exit} from 'node:process';
|
||||||
|
|
||||||
|
const allowedTypes = 'build, chore, ci, docs, enhance, feat, fix, perf, refactor, revert, style, test';
|
||||||
|
const title = env.PR_TITLE;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
console.error('Missing PR_TITLE');
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validTitlePattern = new RegExp(`^(${allowedTypes.replaceAll(', ', '|')})(\\([\\w.-]+\\))?(!)?: .+$`);
|
||||||
|
|
||||||
|
if (!validTitlePattern.test(title)) {
|
||||||
|
console.error(`Invalid PR title: ${title}`);
|
||||||
|
console.error('Expected format: type(scope): subject');
|
||||||
|
console.error(`Allowed types: ${allowedTypes}`);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user