mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-07-23 21:17:45 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c4957e38 | ||
|
|
b1a02cdd5d | ||
|
|
0fd8602ac3 |
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: goreleaser
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0 # all history for all branches and tags
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Import GPG key
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
DOCKER_CONFIG: /tmp/docker-noauth
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: prepare anonymous docker config
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN make clean && make build
|
||||
### DIND VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:29.6.1-dind AS dind
|
||||
FROM docker:29.6.2-dind AS dind
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
@@ -37,7 +37,7 @@ ENTRYPOINT ["s6-svscan","/etc/s6"]
|
||||
### DIND-ROOTLESS VARIANT
|
||||
#
|
||||
#
|
||||
FROM docker:29.6.1-dind-rootless AS dind-rootless
|
||||
FROM docker:29.6.2-dind-rootless AS dind-rootless
|
||||
|
||||
ARG VERSION=dev
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@ Run one dedicated `gitea-runner cache-server` that all runners point at.
|
||||
dir: /data/actcache
|
||||
port: 8088
|
||||
external_secret: "replace-with-a-strong-random-secret"
|
||||
# external_secret_file: /path/to/secret # secret can also be passed via a file
|
||||
```
|
||||
|
||||
2. Start the server:
|
||||
@@ -238,6 +239,7 @@ Run one dedicated `gitea-runner cache-server` that all runners point at.
|
||||
cache:
|
||||
external_server: "http://<cache-server-host>:8088/"
|
||||
external_secret: "replace-with-a-strong-random-secret" # must match the server
|
||||
# external_secret_file: /path/to/secret # secret can also be passed via a file
|
||||
```
|
||||
|
||||
Alternatively, mount the same NFS/CIFS share on every runner and point `cache.dir` at it — simpler, but with weaker isolation between repositories.
|
||||
|
||||
@@ -50,7 +50,7 @@ func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *co
|
||||
|
||||
secret := cfg.Cache.ExternalSecret
|
||||
if secret == "" {
|
||||
return errors.New("cache.external_secret must be set for cache-server; configure the same value on each runner that points at this server via cache.external_server")
|
||||
return errors.New("cache.external_secret (or cache.external_secret_file) must be set for cache-server; configure the same value on each runner that points at this server via cache.external_server")
|
||||
}
|
||||
cacheHandler, err := artifactcache.StartHandler(
|
||||
dir,
|
||||
|
||||
@@ -89,6 +89,7 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
|
||||
if cfg.Cache.ExternalServer != "" {
|
||||
envs["ACTIONS_CACHE_URL"] = cfg.Cache.ExternalServer
|
||||
} else {
|
||||
warnIgnoredCacheSecret(cfg)
|
||||
handler, err := artifactcache.StartHandler(
|
||||
cfg.Cache.Dir,
|
||||
cfg.Cache.Host,
|
||||
@@ -512,14 +513,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
||||
// function the caller must invoke (typically via defer) to revoke the
|
||||
// credential when the task finishes.
|
||||
//
|
||||
// Three modes:
|
||||
// Two modes:
|
||||
// - Embedded handler: register in-process via RegisterJob.
|
||||
// - external_server + external_secret: POST to the remote server's
|
||||
// /_internal/register, defer a POST to /_internal/revoke. This is what
|
||||
// enables full per-job auth and repo scoping over the network.
|
||||
// - external_server alone (no secret): no-op revoker. The remote server is
|
||||
// in legacy openMode and ignores the runtime token; trust is at the
|
||||
// network layer.
|
||||
// - external_server: POST to the remote server's /_internal/register, defer a
|
||||
// POST to /_internal/revoke. This is what enables full per-job auth and
|
||||
// repo scoping over the network.
|
||||
//
|
||||
// Safe with an empty token (older Gitea did not issue one).
|
||||
func (r *Runner) registerCacheForTask(token, repo string, reporter *report.Reporter) func() {
|
||||
@@ -532,6 +530,7 @@ func (r *Runner) registerCacheForTask(token, repo string, reporter *report.Repor
|
||||
if r.cfg.Cache.ExternalServer != "" && r.cfg.Cache.ExternalSecret != "" {
|
||||
return r.registerExternalCacheJob(token, repo, reporter)
|
||||
}
|
||||
// No cache server to register against: caching is disabled, or the built-in server failed to start.
|
||||
return func() {}
|
||||
}
|
||||
|
||||
@@ -655,3 +654,20 @@ func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Respons
|
||||
Capabilities: RunnerCapabilities(),
|
||||
}))
|
||||
}
|
||||
|
||||
// warnIgnoredCacheSecret flags an external cache server secret configured on a runner that uses the built-in cache server.
|
||||
func warnIgnoredCacheSecret(cfg *config.Config) {
|
||||
if cfg.Cache.ExternalServer != "" {
|
||||
return
|
||||
}
|
||||
// Not using an external cache server, so any configured secret is ignored.
|
||||
if cfg.Cache.ExternalSecret == "" {
|
||||
return
|
||||
}
|
||||
// LoadDefault resolves external_secret_file into ExternalSecret, so report whichever key the operator actually wrote.
|
||||
key := "cache.external_secret"
|
||||
if cfg.Cache.ExternalSecretFile != "" {
|
||||
key = "cache.external_secret_file"
|
||||
}
|
||||
log.Warnf("%s is set but cache.external_server is not; the built-in cache server does not use a shared secret, so the value is ignored", key)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,11 @@ cache:
|
||||
# Required when external_server is set. Must be identical on every runner and the cache-server.
|
||||
# Generate with: openssl rand -hex 32
|
||||
external_secret: ""
|
||||
# Path to a file containing the shared secret, as an alternative to external_secret.
|
||||
# Use this to keep the secret out of this file.
|
||||
# Surrounding whitespace is trimmed, so a trailing newline in the file is fine.
|
||||
# Setting both external_secret and external_secret_file is an error.
|
||||
external_secret_file: ""
|
||||
# When true, reuse a cached action instead of fetching from the remote on every job.
|
||||
# A moved tag (e.g. a re-tagged "v6") or an updated branch stays at the cached commit
|
||||
# until its cache entry expires or is manually removed.
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
@@ -57,13 +58,14 @@ type Runner struct {
|
||||
|
||||
// Cache represents the configuration for caching.
|
||||
type Cache struct {
|
||||
Enabled *bool `yaml:"enabled"` // Enabled indicates whether caching is enabled. It is a pointer to distinguish between false and not set. If not set, it will be true.
|
||||
Dir string `yaml:"dir"` // Dir specifies the directory path for caching.
|
||||
Host string `yaml:"host"` // Host specifies the caching host.
|
||||
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
||||
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.
|
||||
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.
|
||||
Enabled *bool `yaml:"enabled"` // Enabled indicates whether caching is enabled. It is a pointer to distinguish between false and not set. If not set, it will be true.
|
||||
Dir string `yaml:"dir"` // Dir specifies the directory path for caching.
|
||||
Host string `yaml:"host"` // Host specifies the caching host.
|
||||
Port uint16 `yaml:"port"` // Port specifies the caching port.
|
||||
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. Required whenever ExternalServer is set; ExternalSecretFile is the alternative way to provide it.
|
||||
ExternalSecretFile string `yaml:"external_secret_file"` // ExternalSecretFile is the path to a file holding the ExternalSecret value, so the secret can be mounted instead of stored in the config file. LoadDefault reads it into ExternalSecret; setting both is an error.
|
||||
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.
|
||||
@@ -177,6 +179,10 @@ func LoadDefault(file string) (*Config, error) {
|
||||
b := true
|
||||
cfg.Cache.Enabled = &b
|
||||
}
|
||||
// Resolved regardless of cache.enabled, because the `cache-server` command reads the secret from the same key without checking cache.enabled.
|
||||
if err := resolveCacheExternalSecret(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *cfg.Cache.Enabled {
|
||||
if cfg.Cache.Dir == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
@@ -186,7 +192,7 @@ func LoadDefault(file string) (*Config, error) {
|
||||
cfg.Cache.Dir = filepath.Join(home, ".cache", "actcache")
|
||||
}
|
||||
if cfg.Cache.ExternalServer != "" && cfg.Cache.ExternalSecret == "" {
|
||||
return nil, errors.New("cache.external_server is set but cache.external_secret is empty; configure the same external_secret on this runner and the gitea-runner cache-server")
|
||||
return nil, errors.New("cache.external_server is set but no shared secret is configured; set cache.external_secret (or cache.external_secret_file) to the same value used by the gitea-runner cache-server")
|
||||
}
|
||||
}
|
||||
if cfg.Container.WorkdirParent == "" {
|
||||
@@ -301,3 +307,24 @@ func definedRunnerConfigKeys(content []byte) (map[string]bool, error) {
|
||||
|
||||
return defined, nil
|
||||
}
|
||||
|
||||
// resolveCacheExternalSecret loads cache.external_secret from the file named by cache.external_secret_file,
|
||||
// so deployments can mount the secret instead of committing it to the config file.
|
||||
func resolveCacheExternalSecret(cfg *Config) error {
|
||||
if cfg.Cache.ExternalSecretFile == "" {
|
||||
return nil
|
||||
}
|
||||
if cfg.Cache.ExternalSecret != "" {
|
||||
return errors.New("cache.external_secret and cache.external_secret_file are both set; configure only one of them")
|
||||
}
|
||||
content, err := os.ReadFile(cfg.Cache.ExternalSecretFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read cache.external_secret_file %q: %w", cfg.Cache.ExternalSecretFile, err)
|
||||
}
|
||||
secret := strings.TrimSpace(string(content))
|
||||
if secret == "" {
|
||||
return fmt.Errorf("cache.external_secret_file %q contains no secret", cfg.Cache.ExternalSecretFile)
|
||||
}
|
||||
cfg.Cache.ExternalSecret = secret
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -227,3 +227,91 @@ func TestContainerNetworkCreateOptions(t *testing.T) {
|
||||
assert.Nil(t, opts.EnableIPv6)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadDefault_ReadsExternalSecretFromFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
secretPath := filepath.Join(dir, "cache.secret")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte(" s3cr3t\n"), 0o600))
|
||||
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`
|
||||
cache:
|
||||
enabled: true
|
||||
external_server: "http://cache.invalid/"
|
||||
external_secret_file: "`+secretPath+`"
|
||||
`), 0o600))
|
||||
|
||||
cfg, err := LoadDefault(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "s3cr3t", cfg.Cache.ExternalSecret)
|
||||
}
|
||||
|
||||
func TestLoadDefault_ReadsExternalSecretFromFileWhenCacheDisabled(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
secretPath := filepath.Join(dir, "cache.secret")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte("s3cr3t"), 0o600))
|
||||
|
||||
// the file has to be resolved even when cache is disabled
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`
|
||||
cache:
|
||||
enabled: false
|
||||
external_secret_file: "`+secretPath+`"
|
||||
`), 0o600))
|
||||
|
||||
cfg, err := LoadDefault(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "s3cr3t", cfg.Cache.ExternalSecret)
|
||||
}
|
||||
|
||||
func TestLoadDefault_RejectsBothExternalSecretAndFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
secretPath := filepath.Join(dir, "cache.secret")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte("s3cr3t"), 0o600))
|
||||
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`
|
||||
cache:
|
||||
enabled: true
|
||||
external_server: "http://cache.invalid/"
|
||||
external_secret: "inline"
|
||||
external_secret_file: "`+secretPath+`"
|
||||
`), 0o600))
|
||||
|
||||
_, err := LoadDefault(path)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "both set")
|
||||
}
|
||||
|
||||
func TestLoadDefault_RejectsMissingExternalSecretFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`
|
||||
cache:
|
||||
enabled: true
|
||||
external_server: "http://cache.invalid/"
|
||||
external_secret_file: "`+filepath.Join(dir, "absent.secret")+`"
|
||||
`), 0o600))
|
||||
|
||||
_, err := LoadDefault(path)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "read cache.external_secret_file")
|
||||
}
|
||||
|
||||
func TestLoadDefault_RejectsEmptyExternalSecretFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
secretPath := filepath.Join(dir, "cache.secret")
|
||||
require.NoError(t, os.WriteFile(secretPath, []byte("\n \n"), 0o600))
|
||||
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(`
|
||||
cache:
|
||||
enabled: true
|
||||
external_server: "http://cache.invalid/"
|
||||
external_secret_file: "`+secretPath+`"
|
||||
`), 0o600))
|
||||
|
||||
_, err := LoadDefault(path)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "contains no secret")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user