mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-05 23:05:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a8ee4ce57 | ||
|
|
bf03f36d5a | ||
|
|
5a38a3d0c4 | ||
|
|
6618e67645 | ||
|
|
2b599fae7f | ||
|
|
990ebaa2ac |
No files matched your search
@@ -31,7 +31,7 @@ jobs:
|
|||||||
- name: pre-pull test images
|
- name: pre-pull test images
|
||||||
env:
|
env:
|
||||||
TEST_JOB_IMAGE: node:24-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 # renovate: datasource=docker
|
TEST_JOB_IMAGE: node:24-bookworm-slim@sha256:0e0ff40c39bc087845bfb27465a0df4ea419520094bc35842ff83dd8cbe6f9b6 # renovate: datasource=docker
|
||||||
TEST_SERVICE_IMAGE: nginx:alpine@sha256:62ff2089abf5a9ed33bd232895bef5e22f7bb4b200675cec49a5ebc48e3d4ac8 # renovate: datasource=docker
|
TEST_SERVICE_IMAGE: nginx:alpine@sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2 # renovate: datasource=docker
|
||||||
run: |
|
run: |
|
||||||
for image in "$TEST_JOB_IMAGE" "$TEST_SERVICE_IMAGE"; do
|
for image in "$TEST_JOB_IMAGE" "$TEST_SERVICE_IMAGE"; do
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SHASUM ?= shasum -a 256
|
|||||||
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
|
||||||
XGO_PACKAGE ?= src.techknowlogick.com/[email protected] # renovate: datasource=go
|
XGO_PACKAGE ?= src.techknowlogick.com/[email protected] # renovate: datasource=go
|
||||||
XGO_VERSION := go-1.27.x
|
XGO_VERSION := go-1.27.x
|
||||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/[email protected]6 # renovate: datasource=go
|
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/[email protected]7 # renovate: datasource=go
|
||||||
|
|
||||||
PODMAN_TEST_IMAGE ?= quay.io/containers/podman:v5.8.4-immutable@sha256:90405e3c6a86f33c5a6f8dbdfa8e02a98143a8ba4337b84bdaec5336d2ef008a # renovate: datasource=docker
|
PODMAN_TEST_IMAGE ?= quay.io/containers/podman:v5.8.4-immutable@sha256:90405e3c6a86f33c5a6f8dbdfa8e02a98143a8ba4337b84bdaec5336d2ef008a # renovate: datasource=docker
|
||||||
E2E_JOB_IMAGE ?= node:24-bookworm@sha256:64af3819f9275802414d7cdc38c27e9d82bd564dec4d4da87d008255d36c63b4 # renovate: datasource=docker
|
E2E_JOB_IMAGE ?= node:24-bookworm@sha256:64af3819f9275802414d7cdc38c27e9d82bd564dec4d4da87d008255d36c63b4 # renovate: datasource=docker
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ These hosts are added to `no_proxy` for jobs, so they are always reached directl
|
|||||||
|
|
||||||
Gitea is not added. Add it to `no_proxy` yourself if it should be reached directly.
|
Gitea is not added. Add it to `no_proxy` yourself if it should be reached directly.
|
||||||
|
|
||||||
To change a value for one job, set it in a step's `env:` or in the job's `container.env`. Setting it at workflow or job level has no effect. To change it for the whole runner, set it in `runner.envs`. A `no_proxy` set there is added to the list above instead of replacing it.
|
For one step, set it in the step's `env:`, for the rest of a job, write it to `$GITHUB_ENV`. Setting it at workflow or job level or in `container.env` has no effect. To change it for the whole runner, set it in `runner.envs`. A `no_proxy` set there is added to the list above instead of replacing it.
|
||||||
|
|
||||||
Images are pulled by the Docker daemon, which needs its own proxy setting. In the `dind` images the daemon runs in the same container and reads the variables above. For any other daemon, see [the Docker documentation](https://docs.docker.com/engine/daemon/proxy/). The runner logs a warning at startup if it has a proxy and the daemon does not.
|
Images are pulled by the Docker daemon, which needs its own proxy setting. In the `dind` images the daemon runs in the same container and reads the variables above. For any other daemon, see [the Docker documentation](https://docs.docker.com/engine/daemon/proxy/). The runner logs a warning at startup if it has a proxy and the daemon does not.
|
||||||
|
|
||||||
|
|||||||
@@ -593,7 +593,8 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
|
|||||||
|
|
||||||
// If the `privileged` config has been disabled, `copts.privileged` need to be forced to false,
|
// If the `privileged` config has been disabled, `copts.privileged` need to be forced to false,
|
||||||
// even if the user specifies `--privileged` in the options string.
|
// even if the user specifies `--privileged` in the options string.
|
||||||
if !hostConfig.Privileged {
|
if !hostConfig.Privileged && copts.privileged {
|
||||||
|
logger.Warnf("container option %q is not allowed when privileged mode is disabled and will be ignored", "--privileged")
|
||||||
copts.privileged = false
|
copts.privileged = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,6 +630,7 @@ func (cr *containerReference) mergeContainerConfigs(ctx context.Context, config
|
|||||||
|
|
||||||
logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig)
|
logger.Debugf("Custom container.HostConfig from options ==> %+v", containerConfig.HostConfig)
|
||||||
|
|
||||||
|
cr.sanitizeConfig(ctx, nil, containerConfig.HostConfig) // a dropped volume must not displace the runner's own
|
||||||
overlayVolumes(hostConfig, containerConfig.HostConfig)
|
overlayVolumes(hostConfig, containerConfig.HostConfig)
|
||||||
binds := hostConfig.Binds
|
binds := hostConfig.Binds
|
||||||
mounts := hostConfig.Mounts
|
mounts := hostConfig.Mounts
|
||||||
@@ -1261,6 +1263,12 @@ func (cr *containerReference) sanitizeConfig(ctx context.Context, config *contai
|
|||||||
return config, hostConfig
|
return config, hostConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SanitizeBinds(ctx context.Context, validVolumes, binds []string) []string {
|
||||||
|
cr := &containerReference{input: &NewContainerInput{ValidVolumes: validVolumes}}
|
||||||
|
_, hostConfig := cr.sanitizeConfig(ctx, nil, &container.HostConfig{Binds: binds})
|
||||||
|
return hostConfig.Binds
|
||||||
|
}
|
||||||
|
|
||||||
// bindTarget returns the container path a bind mounts onto, empty if it cannot be parsed.
|
// bindTarget returns the container path a bind mounts onto, empty if it cannot be parsed.
|
||||||
func bindTarget(bind string) string {
|
func bindTarget(bind string) string {
|
||||||
parsed, err := loader.ParseVolume(bind)
|
parsed, err := loader.ParseVolume(bind)
|
||||||
|
|||||||
@@ -682,6 +682,7 @@ func mergeOptions(t *testing.T, runnerOptions, workflowOptions string, privilege
|
|||||||
WorkflowOptions: workflowOptions,
|
WorkflowOptions: workflowOptions,
|
||||||
NetworkMode: "bridge",
|
NetworkMode: "bridge",
|
||||||
UsernsMode: "private",
|
UsernsMode: "private",
|
||||||
|
ValidVolumes: []string{"**"},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
_, hostConfig, err := cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{
|
_, hostConfig, err := cr.mergeContainerConfigs(common.WithLogger(context.Background(), logger), &container.Config{}, &container.HostConfig{
|
||||||
@@ -844,8 +845,10 @@ func TestMergeContainerConfigsVolumesReplaceRunnerMounts(t *testing.T) {
|
|||||||
ctx := common.WithLogger(context.Background(), logger)
|
ctx := common.WithLogger(context.Background(), logger)
|
||||||
cr := &containerReference{
|
cr := &containerReference{
|
||||||
input: &NewContainerInput{
|
input: &NewContainerInput{
|
||||||
NetworkMode: "bridge",
|
NetworkMode: "bridge",
|
||||||
RunnerOptions: "--volume /host/tools:/opt/hostedtoolcache",
|
RunnerOptions: "--volume /host/tools:/opt/hostedtoolcache",
|
||||||
|
WorkflowOptions: "--volume /unapproved:/var/run/docker.sock",
|
||||||
|
ValidVolumes: []string{"/host/tools"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,6 +870,7 @@ func TestMergeContainerConfigsWarnsOnlyAboutOptionsThatWereGiven(t *testing.T) {
|
|||||||
assert.Zero(t, warnings("--volume /host/tools:/opt/hostedtoolcache", ""))
|
assert.Zero(t, warnings("--volume /host/tools:/opt/hostedtoolcache", ""))
|
||||||
assert.Zero(t, warnings("", "--shm-size 1g"))
|
assert.Zero(t, warnings("", "--shm-size 1g"))
|
||||||
assert.Equal(t, 1, warnings("--network host", ""))
|
assert.Equal(t, 1, warnings("--network host", ""))
|
||||||
|
assert.Equal(t, 1, warnings("", "--privileged"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeContainerConfigsKeepsNetworkAliasesFromOptions(t *testing.T) {
|
func TestMergeContainerConfigsKeepsNetworkAliasesFromOptions(t *testing.T) {
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ func RemoveOrphanNetworks(_ context.Context, _, _ string, _ time.Time) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SanitizeBinds(_ context.Context, _, binds []string) []string {
|
||||||
|
return binds
|
||||||
|
}
|
||||||
|
|
||||||
func CreateJobVolumes(ctx context.Context, runnerUUID string, volumeNames []string) error {
|
func CreateJobVolumes(ctx context.Context, runnerUUID string, volumeNames []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ func newStepContainer(ctx context.Context, step step, image string, cmd, entrypo
|
|||||||
|
|
||||||
envList = append(envList, rc.runnerEnv(ctx)...)
|
envList = append(envList, rc.runnerEnv(ctx)...)
|
||||||
|
|
||||||
binds, mounts := rc.GetBindsAndMounts()
|
binds, mounts := rc.GetBindsAndMounts(ctx)
|
||||||
networkMode := "container:" + rc.jobContainerName()
|
networkMode := "container:" + rc.jobContainerName()
|
||||||
if rc.IsHostEnv() {
|
if rc.IsHostEnv() {
|
||||||
networkMode = "default"
|
networkMode = "default"
|
||||||
|
|||||||
@@ -248,6 +248,14 @@ func inputsFromEnv(env map[string]string) map[string]any {
|
|||||||
func getEvaluatorInputs(rc *RunContext, stepInputs map[string]any, ghc *model.GithubContext) map[string]any {
|
func getEvaluatorInputs(rc *RunContext, stepInputs map[string]any, ghc *model.GithubContext) map[string]any {
|
||||||
inputs := map[string]any{}
|
inputs := map[string]any{}
|
||||||
|
|
||||||
|
// only a called workflow's job itself takes the inputs Gitea resolved,
|
||||||
|
// composite actions copy its Config but have their own `inputs`
|
||||||
|
if rc.Config.WorkflowCallInputs != nil && rc.Parent == nil {
|
||||||
|
maps.Copy(inputs, rc.Config.WorkflowCallInputs)
|
||||||
|
maps.Copy(inputs, stepInputs)
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
maps.Copy(inputs, rc.workflowCallInputs)
|
maps.Copy(inputs, rc.workflowCallInputs)
|
||||||
maps.Copy(inputs, stepInputs)
|
maps.Copy(inputs, stepInputs)
|
||||||
|
|
||||||
|
|||||||
@@ -392,6 +392,28 @@ jobs:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetEvaluatorInputsResolvedByGitea(t *testing.T) {
|
||||||
|
wf, err := model.ReadWorkflow(strings.NewReader(`
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
name: {type: string, default: callee}
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
name: {type: string}
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
config := &Config{Workdir: ".", WorkflowCallInputs: map[string]any{"name": "caller", "target": "dispatch"}}
|
||||||
|
ghc := &model.GithubContext{EventName: "workflow_dispatch", Event: map[string]any{"inputs": map[string]any{"name": "dispatch"}}}
|
||||||
|
|
||||||
|
// the event's own inputs must not override those Gitea resolved
|
||||||
|
job := &RunContext{Config: config, Run: &model.Run{JobID: "job1", Workflow: wf}}
|
||||||
|
assert.Equal(t, map[string]any{"name": "caller", "target": "dispatch"}, getEvaluatorInputs(job, nil, ghc))
|
||||||
|
|
||||||
|
composite := &RunContext{Config: config, Run: &model.Run{JobID: "job1", Workflow: &model.Workflow{}}, Parent: job}
|
||||||
|
assert.Equal(t, map[string]any{"arg": "x"}, getEvaluatorInputs(composite, map[string]any{"arg": "x"}, ghc))
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolveWorkflowCallToleratesMismatchesAndPassesInheritedSecretsVerbatim(t *testing.T) {
|
func TestResolveWorkflowCallToleratesMismatchesAndPassesInheritedSecretsVerbatim(t *testing.T) {
|
||||||
callerWorkflow, err := model.ReadWorkflow(strings.NewReader(`
|
callerWorkflow, err := model.ReadWorkflow(strings.NewReader(`
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -435,11 +435,7 @@ func evaluateJobEnvAndDefaults(ctx context.Context, rc *RunContext) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rc.jobRunDefaults = defaults.Run
|
rc.jobRunDefaults = defaults.Run
|
||||||
if rc.containerSpec.Image == "" {
|
return model.DecodeEvaluated("container env", rc.containerEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &rc.containerSpec.Env)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
_, containerEnv := splitContainerEnv(rc.Run.Job().RawContainer)
|
|
||||||
return model.DecodeEvaluated("container env", containerEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &rc.containerSpec.Env)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setJobOutputs(ctx context.Context, rc *RunContext) error {
|
func setJobOutputs(ctx context.Context, rc *RunContext) error {
|
||||||
|
|||||||
+64
-16
@@ -64,7 +64,8 @@ type RunContext struct {
|
|||||||
ExprEval *expressionEvaluator
|
ExprEval *expressionEvaluator
|
||||||
JobContainer container.ExecutionsEnvironment
|
JobContainer container.ExecutionsEnvironment
|
||||||
serviceContainers []*serviceContainer
|
serviceContainers []*serviceContainer
|
||||||
containerSpec model.ContainerSpec // container:, resolved once with platformImage, zero without a job container
|
containerSpec model.ContainerSpec // container:, resolved once with platformImage
|
||||||
|
containerEnv yaml.Node // container.env of the kept containerSpec, decoded once the job env resolves
|
||||||
JobName string
|
JobName string
|
||||||
ActionPath string
|
ActionPath string
|
||||||
Parent *RunContext
|
Parent *RunContext
|
||||||
@@ -113,6 +114,7 @@ type serviceContainer struct {
|
|||||||
name string
|
name string
|
||||||
image string
|
image string
|
||||||
container container.ExecutionsEnvironment
|
container container.ExecutionsEnvironment
|
||||||
|
created bool
|
||||||
logsDumped bool
|
logsDumped bool
|
||||||
info *container.Info // last poll, the source of the `job.services` entry
|
info *container.Info // last poll, the source of the `job.services` entry
|
||||||
}
|
}
|
||||||
@@ -334,12 +336,12 @@ func splitVolumes(specs []string) ([]string, map[string]string, map[string]bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the binds and mounts for the container, resolving paths as appopriate
|
// Returns the binds and mounts for the container, resolving paths as appopriate
|
||||||
func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
|
func (rc *RunContext) GetBindsAndMounts(ctx context.Context) ([]string, map[string]string) {
|
||||||
name := rc.jobContainerName()
|
name := rc.jobContainerName()
|
||||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||||
|
|
||||||
// the runner's own mounts below yield to the targets the job claims
|
// the runner's own mounts below yield to the targets the job claims
|
||||||
binds, mounts, claimed := splitVolumes(rc.containerSpec.Volumes)
|
binds, mounts, claimed := splitVolumes(container.SanitizeBinds(ctx, rc.validVolumes(), rc.containerSpec.Volumes))
|
||||||
|
|
||||||
if rc.containerDaemonSocket() != "-" && !claimed["/var/run/docker.sock"] {
|
if rc.containerDaemonSocket() != "-" && !claimed["/var/run/docker.sock"] {
|
||||||
binds = append(binds, rc.jobDockerSocket()+":/var/run/docker.sock")
|
binds = append(binds, rc.jobDockerSocket()+":/var/run/docker.sock")
|
||||||
@@ -497,10 +499,12 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
if err := model.DecodeEvaluated("job services", rc.Run.Job().RawServices, rc.ExprEval.shared(ctx).EvaluateYamlNode, &services); err != nil {
|
if err := model.DecodeEvaluated("job services", rc.Run.Job().RawServices, rc.ExprEval.shared(ctx).EvaluateYamlNode, &services); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for serviceID, spec := range services {
|
for _, serviceID := range slices.Sorted(maps0.Keys(services)) {
|
||||||
|
spec := services[serviceID]
|
||||||
if spec == nil {
|
if spec == nil {
|
||||||
return fmt.Errorf("service %s has no container definition", serviceID)
|
return fmt.Errorf("service %s has no container definition", serviceID)
|
||||||
}
|
}
|
||||||
|
spec.Image = strings.TrimPrefix(spec.Image, "docker://")
|
||||||
// GitHub compatibility: skip services whose image evaluates to an
|
// GitHub compatibility: skip services whose image evaluates to an
|
||||||
// empty string, enabling conditional services via expressions
|
// empty string, enabling conditional services via expressions
|
||||||
if spec.Image == "" {
|
if spec.Image == "" {
|
||||||
@@ -537,6 +541,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
|
serviceContainerName := createContainerName(rc.jobContainerName(), serviceID)
|
||||||
|
serviceLog := common.NewLineWriter(rawLogLine(ctx)) // raw, so a service's output cannot set the job's env, outputs or state
|
||||||
c := newContainer(&container.NewContainerInput{
|
c := newContainer(&container.NewContainerInput{
|
||||||
Name: serviceContainerName,
|
Name: serviceContainerName,
|
||||||
Image: spec.Image,
|
Image: spec.Image,
|
||||||
@@ -547,8 +552,8 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
Env: envs,
|
Env: envs,
|
||||||
Mounts: serviceMounts,
|
Mounts: serviceMounts,
|
||||||
Binds: serviceBinds,
|
Binds: serviceBinds,
|
||||||
Stdout: logWriter,
|
Stdout: serviceLog,
|
||||||
Stderr: logWriter,
|
Stderr: serviceLog,
|
||||||
Privileged: rc.Config.Privileged,
|
Privileged: rc.Config.Privileged,
|
||||||
UsernsMode: rc.Config.UsernsMode,
|
UsernsMode: rc.Config.UsernsMode,
|
||||||
Platform: rc.Config.ContainerArchitecture,
|
Platform: rc.Config.ContainerArchitecture,
|
||||||
@@ -570,7 +575,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
ext := container.LinuxContainerEnvironmentExtensions{}
|
ext := container.LinuxContainerEnvironmentExtensions{}
|
||||||
containerInput := &container.NewContainerInput{
|
containerInput := &container.NewContainerInput{
|
||||||
Cmd: nil,
|
Cmd: nil,
|
||||||
Entrypoint: []string{"/bin/sleep", fmt.Sprint(rc.Config.ContainerMaxLifetime.Round(time.Second).Seconds())},
|
Entrypoint: []string{"sleep", fmt.Sprint(rc.Config.ContainerMaxLifetime.Round(time.Second).Seconds())},
|
||||||
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
WorkingDir: ext.ToContainerPath(rc.Config.Workdir),
|
||||||
Image: image,
|
Image: image,
|
||||||
Username: username,
|
Username: username,
|
||||||
@@ -602,7 +607,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rc.startDockerProxy(ctx)
|
rc.startDockerProxy(ctx)
|
||||||
containerInput.Binds, containerInput.Mounts = rc.GetBindsAndMounts()
|
containerInput.Binds, containerInput.Mounts = rc.GetBindsAndMounts(ctx)
|
||||||
containerInput.ValidVolumes = rc.validVolumes()
|
containerInput.ValidVolumes = rc.validVolumes()
|
||||||
|
|
||||||
rc.jobNetworkName = networkName
|
rc.jobNetworkName = networkName
|
||||||
@@ -633,16 +638,21 @@ func (rc *RunContext) startJobContainer() common.Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) commandLogWriter(ctx context.Context) io.Writer {
|
func (rc *RunContext) commandLogWriter(ctx context.Context) io.Writer {
|
||||||
|
return common.NewLineWriter(rc.commandHandler(ctx), rawLogLine(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawLogLine(ctx context.Context) common.LineHandler {
|
||||||
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
return common.NewLineWriter(rc.commandHandler(ctx), func(line string) bool {
|
return func(line string) bool {
|
||||||
rawLogger.Infof("%s", line)
|
rawLogger.Infof("%s", line)
|
||||||
return true
|
return true
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNetwork, preclean bool) common.Executor {
|
func (rc *RunContext) cleanupJobResources(networkName string, createAndDeleteNetwork, preclean bool) common.Executor {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
logger := common.Logger(ctx)
|
logger := common.Logger(ctx)
|
||||||
|
rc.printServiceLogs(ctx)
|
||||||
errs := []error{rc.closeDockerProxy(ctx)}
|
errs := []error{rc.closeDockerProxy(ctx)}
|
||||||
if rc.JobContainer != nil {
|
if rc.JobContainer != nil {
|
||||||
errs = append(errs, rc.JobContainer.Remove()(ctx))
|
errs = append(errs, rc.JobContainer.Remove()(ctx))
|
||||||
@@ -789,6 +799,7 @@ func (rc *RunContext) startServiceContainers() common.Executor {
|
|||||||
for _, svc := range rc.serviceContainers {
|
for _, svc := range rc.serviceContainers {
|
||||||
execs = append(execs, common.NewPipelineExecutor(
|
execs = append(execs, common.NewPipelineExecutor(
|
||||||
svc.container.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
|
svc.container.Create(rc.Config.ContainerCapAdd, rc.Config.ContainerCapDrop),
|
||||||
|
func(context.Context) error { svc.created = true; return nil },
|
||||||
svc.container.Start(false),
|
svc.container.Start(false),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -927,6 +938,26 @@ func (svc *serviceContainer) dumpLogs(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serviceLogsTimeout = 15 * time.Second // cleanup gets one minute in total
|
||||||
|
|
||||||
|
func (rc *RunContext) printServiceLogs(ctx context.Context) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, serviceLogsTimeout)
|
||||||
|
defer cancel()
|
||||||
|
rawLogger := common.Logger(ctx).WithField(rawOutputField, true)
|
||||||
|
for _, svc := range rc.serviceContainers {
|
||||||
|
if !svc.created || svc.logsDumped {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rawLogger.Infof("::group::Print service container logs: %s", EscapeCommandData(svc.name))
|
||||||
|
svc.dumpLogs(ctx)
|
||||||
|
rawLogger.Infof("::endgroup::")
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
common.Logger(ctx).Warnf("Timed out printing the service logs after %s", serviceLogsTimeout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// inspect also records the state for the `job.services` context.
|
// inspect also records the state for the `job.services` context.
|
||||||
func (svc *serviceContainer) inspect(ctx context.Context) (*container.Info, error) {
|
func (svc *serviceContainer) inspect(ctx context.Context) (*container.Info, error) {
|
||||||
info, err := svc.container.Inspect(ctx)
|
info, err := svc.container.Inspect(ctx)
|
||||||
@@ -1039,7 +1070,7 @@ func (rc *RunContext) cleanupFailedStart(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) IsHostEnv() bool {
|
func (rc *RunContext) IsHostEnv() bool {
|
||||||
return strings.EqualFold(rc.platformImage, "-self-hosted")
|
return rc.containerSpec.Image == "" && strings.EqualFold(rc.platformImage, "-self-hosted")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rc *RunContext) stopContainer() common.Executor {
|
func (rc *RunContext) stopContainer() common.Executor {
|
||||||
@@ -1159,15 +1190,19 @@ func (rc *RunContext) runsOnPlatformNames(ctx context.Context) []string {
|
|||||||
return model.RunsOnFromNode(rawRunsOn)
|
return model.RunsOnFromNode(rawRunsOn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvePlatformImage evaluates the job's container once for every consumer, ignoring one without an image as GitHub does.
|
// resolvePlatformImage evaluates the job's container once for every consumer, skipping one whose image is empty as GitHub does.
|
||||||
func (rc *RunContext) resolvePlatformImage(ctx context.Context) error {
|
func (rc *RunContext) resolvePlatformImage(ctx context.Context) error {
|
||||||
withoutEnv, _ := splitContainerEnv(rc.Run.Job().RawContainer)
|
withoutEnv, env := splitContainerEnv(rc.Run.Job().RawContainer)
|
||||||
var spec *model.ContainerSpec
|
var spec model.ContainerSpec
|
||||||
if err := model.DecodeEvaluated("container", withoutEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &spec); err != nil {
|
if err := model.DecodeEvaluated("container", withoutEnv, rc.ExprEval.shared(ctx).EvaluateYamlNode, &spec); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if spec != nil && spec.Image != "" {
|
spec.Image = strings.TrimPrefix(spec.Image, "docker://")
|
||||||
rc.containerSpec, rc.platformImage = *spec, spec.Image
|
if spec.Image != "" || imagelessContainer(withoutEnv) {
|
||||||
|
rc.containerSpec, rc.containerEnv = spec, env
|
||||||
|
}
|
||||||
|
if rc.containerSpec.Image != "" {
|
||||||
|
rc.platformImage = rc.containerSpec.Image
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
image, err := rc.runsOnImage(ctx)
|
image, err := rc.runsOnImage(ctx)
|
||||||
@@ -1175,6 +1210,19 @@ func (rc *RunContext) resolvePlatformImage(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// imagelessContainer reports a container: mapping without an image key, which configures the runs-on image.
|
||||||
|
func imagelessContainer(container yaml.Node) bool {
|
||||||
|
if container.Kind != yaml.MappingNode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(container.Content); i += 2 {
|
||||||
|
if container.Content[i].Value == "image" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// splitContainerEnv takes `env` out of a container mapping, since env reads the job env that resolves after the job environment starts.
|
// splitContainerEnv takes `env` out of a container mapping, since env reads the job env that resolves after the job environment starts.
|
||||||
func splitContainerEnv(container yaml.Node) (withoutEnv, env yaml.Node) {
|
func splitContainerEnv(container yaml.Node) (withoutEnv, env yaml.Node) {
|
||||||
if container.Kind != yaml.MappingNode {
|
if container.Kind != yaml.MappingNode {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/moby/moby/api/types/volume"
|
"github.com/moby/moby/api/types/volume"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||||
assert "github.com/stretchr/testify/assert"
|
assert "github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
require "github.com/stretchr/testify/require"
|
require "github.com/stretchr/testify/require"
|
||||||
@@ -342,12 +343,12 @@ jobs:
|
|||||||
require.Nil(t, redis.Cmd)
|
require.Nil(t, redis.Cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStartJobContainerEvaluatesContainersOnce(t *testing.T) {
|
func TestStartJobContainerBuildsEvaluatedJobAndServiceInputs(t *testing.T) {
|
||||||
inputs := startJobContainerInputs(t, `
|
inputs := startJobContainerInputs(t, `
|
||||||
jobs:
|
jobs:
|
||||||
job:
|
job:
|
||||||
container: ${{ fromJSON('{"image":"node:20","options":"--label ${{ github.job }}","credentials":{"username":"user","password":"${{ vars.TITLE }}"}}') }}
|
container: ${{ fromJSON('{"image":"docker://node:20","options":"--label ${{ github.job }}","credentials":{"username":"user","password":"${{ vars.TITLE }}"}}') }}
|
||||||
services: ${{ fromJSON('{"redis":{"image":"redis:latest","volumes":["data:/data"],"env":{"TITLE":"${{ github.job }}"},"credentials":{"username":"user","password":"${{ github.job }}"},"entrypoint":"/entry.sh","command":"redis-server --port 6380"}}') }}
|
services: ${{ fromJSON('{"redis":{"image":"docker://redis:latest","volumes":["data:/data"],"env":{"TITLE":"${{ github.job }}"},"credentials":{"username":"user","password":"${{ github.job }}"},"entrypoint":"/entry.sh","command":"redis-server --port 6380"}}') }}
|
||||||
`, &Config{})
|
`, &Config{})
|
||||||
redis, job := inputs[0], inputs[1]
|
redis, job := inputs[0], inputs[1]
|
||||||
|
|
||||||
@@ -358,6 +359,7 @@ jobs:
|
|||||||
require.Equal(t, []string{"/entry.sh"}, redis.Entrypoint)
|
require.Equal(t, []string{"/entry.sh"}, redis.Entrypoint)
|
||||||
require.Equal(t, []string{"redis-server", "--port", "6380"}, redis.Cmd)
|
require.Equal(t, []string{"redis-server", "--port", "6380"}, redis.Cmd)
|
||||||
require.Equal(t, "node:20", job.Image)
|
require.Equal(t, "node:20", job.Image)
|
||||||
|
require.Equal(t, "sleep", job.Entrypoint[0])
|
||||||
require.Equal(t, "--label ${{ github.job }}", job.WorkflowOptions)
|
require.Equal(t, "--label ${{ github.job }}", job.WorkflowOptions)
|
||||||
require.Equal(t, "${{ vars.TITLE }}", job.Password)
|
require.Equal(t, "${{ vars.TITLE }}", job.Password)
|
||||||
}
|
}
|
||||||
@@ -412,17 +414,14 @@ jobs:
|
|||||||
steps: []
|
steps: []
|
||||||
`, &Config{ProxyEnv: map[string]string{"http_proxy": "http://proxy:3128", "no_proxy": "internal.example"}})
|
`, &Config{ProxyEnv: map[string]string{"http_proxy": "http://proxy:3128", "no_proxy": "internal.example"}})
|
||||||
|
|
||||||
env := map[string][]string{}
|
db, redis := inputs[0], inputs[1]
|
||||||
for _, in := range inputs {
|
|
||||||
env[in.Image] = in.Env
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Contains(t, env["redis:latest"], "http_proxy=http://proxy:3128")
|
require.Contains(t, redis.Env, "http_proxy=http://proxy:3128")
|
||||||
require.Contains(t, env["redis:latest"], "no_proxy=internal.example")
|
require.Contains(t, redis.Env, "no_proxy=internal.example")
|
||||||
// the service's own env wins over what the runner injected, without dropping the rest
|
// the service's own env wins over what the runner injected, without dropping the rest
|
||||||
require.Contains(t, env["postgres:latest"], "no_proxy=db-only.example")
|
require.Contains(t, db.Env, "no_proxy=db-only.example")
|
||||||
require.NotContains(t, env["postgres:latest"], "no_proxy=internal.example")
|
require.NotContains(t, db.Env, "no_proxy=internal.example")
|
||||||
require.Contains(t, env["postgres:latest"], "http_proxy=http://proxy:3128")
|
require.Contains(t, db.Env, "http_proxy=http://proxy:3128")
|
||||||
}
|
}
|
||||||
|
|
||||||
// act builds Dockerfile actions through the API, which does not pre-populate the proxy
|
// act builds Dockerfile actions through the API, which does not pre-populate the proxy
|
||||||
@@ -483,7 +482,7 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
config := testcase.rc.Config
|
config := testcase.rc.Config
|
||||||
config.Workdir = testcase.name
|
config.Workdir = testcase.name
|
||||||
config.BindWorkdir = bindWorkDir
|
config.BindWorkdir = bindWorkDir
|
||||||
gotbind, gotmount := rctemplate.GetBindsAndMounts()
|
gotbind, gotmount := rctemplate.GetBindsAndMounts(t.Context())
|
||||||
|
|
||||||
// Name binds/mounts are either/or
|
// Name binds/mounts are either/or
|
||||||
if config.BindWorkdir {
|
if config.BindWorkdir {
|
||||||
@@ -533,7 +532,8 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
BindWorkdir: false,
|
BindWorkdir: false,
|
||||||
|
ValidVolumes: []string{"**"},
|
||||||
Secrets: map[string]string{
|
Secrets: map[string]string{
|
||||||
"MAME": "/host/mame/roms",
|
"MAME": "/host/mame/roms",
|
||||||
},
|
},
|
||||||
@@ -544,7 +544,7 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
rc.ExprEval = rc.NewExpressionEvaluator(context.Background())
|
||||||
require.NoError(t, rc.resolvePlatformImage(context.Background()))
|
require.NoError(t, rc.resolvePlatformImage(context.Background()))
|
||||||
|
|
||||||
gotbind, gotmount := rc.GetBindsAndMounts()
|
gotbind, gotmount := rc.GetBindsAndMounts(t.Context())
|
||||||
assert.Contains(t, gotbind, "/host/mame/roms:/root/.mame/roms:ro")
|
assert.Contains(t, gotbind, "/host/mame/roms:/root/.mame/roms:ro")
|
||||||
assert.NotContains(t, gotbind, "${{ secrets.MAME }}")
|
assert.NotContains(t, gotbind, "${{ secrets.MAME }}")
|
||||||
assert.NotContains(t, gotmount, "${{ secrets.MAME }}")
|
assert.NotContains(t, gotmount, "${{ secrets.MAME }}")
|
||||||
@@ -562,12 +562,13 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
Config: &Config{
|
Config: &Config{
|
||||||
BindWorkdir: false,
|
BindWorkdir: false,
|
||||||
SharedToolCache: true, // so OverridesToolCache has a mount to displace
|
SharedToolCache: true, // so OverridesToolCache has a mount to displace
|
||||||
|
ValidVolumes: []string{"**"},
|
||||||
},
|
},
|
||||||
containerSpec: model.ContainerSpec{Volumes: testcase.volumes},
|
containerSpec: model.ContainerSpec{Volumes: testcase.volumes},
|
||||||
}
|
}
|
||||||
rc.Run.JobID = "job1"
|
rc.Run.JobID = "job1"
|
||||||
|
|
||||||
gotbind, gotmount := rc.GetBindsAndMounts()
|
gotbind, gotmount := rc.GetBindsAndMounts(t.Context())
|
||||||
|
|
||||||
if len(testcase.wantbind) > 0 {
|
if len(testcase.wantbind) > 0 {
|
||||||
assert.Contains(t, gotbind, testcase.wantbind)
|
assert.Contains(t, gotbind, testcase.wantbind)
|
||||||
@@ -602,11 +603,12 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
Config: &Config{},
|
Config: &Config{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, gotmount := rc.GetBindsAndMounts()
|
_, gotmount := rc.GetBindsAndMounts(t.Context())
|
||||||
assert.NotContains(t, gotmount, sharedToolCacheVolume)
|
assert.NotContains(t, gotmount, sharedToolCacheVolume)
|
||||||
|
|
||||||
rc.Config.SharedToolCache = true
|
rc.Config.SharedToolCache = true
|
||||||
_, gotmount = rc.GetBindsAndMounts()
|
rc.containerSpec.Volumes = []string{"/unapproved:" + container.DefaultToolCache}
|
||||||
|
_, gotmount = rc.GetBindsAndMounts(t.Context())
|
||||||
assert.Equal(t, container.DefaultToolCache, gotmount[sharedToolCacheVolume])
|
assert.Equal(t, container.DefaultToolCache, gotmount[sharedToolCacheVolume])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -616,18 +618,18 @@ func TestRunContext_GetBindsAndMounts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
rc := &RunContext{
|
rc := &RunContext{
|
||||||
Run: &model.Run{JobID: "job1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job1": {}}}},
|
Run: &model.Run{JobID: "job1", Workflow: &model.Workflow{Jobs: map[string]*model.Job{"job1": {}}}},
|
||||||
Config: &Config{BindWorkdir: true, Workdir: "/workspace/1/owner/repo", PresetGitHubContext: &model.GithubContext{}},
|
Config: &Config{BindWorkdir: true, Workdir: "/workspace/1/owner/repo", PresetGitHubContext: &model.GithubContext{}, ValidVolumes: []string{"claimed"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
gotbind, _ := rc.GetBindsAndMounts()
|
gotbind, _ := rc.GetBindsAndMounts(t.Context())
|
||||||
assert.True(t, slices.ContainsFunc(gotbind, func(bind string) bool { return strings.HasPrefix(bind, "/workspace/1:/workspace/1") }), gotbind)
|
assert.True(t, slices.ContainsFunc(gotbind, func(bind string) bool { return strings.HasPrefix(bind, "/workspace/1:/workspace/1") }), gotbind)
|
||||||
|
|
||||||
rc.Config.BindWorkdir = false
|
rc.Config.BindWorkdir = false
|
||||||
_, gotmount := rc.GetBindsAndMounts()
|
_, gotmount := rc.GetBindsAndMounts(t.Context())
|
||||||
assert.Equal(t, "/workspace/1", gotmount[rc.jobContainerName()])
|
assert.Equal(t, "/workspace/1", gotmount[rc.jobContainerName()])
|
||||||
|
|
||||||
rc.containerSpec.Volumes = []string{"claimed:/workspace/1"}
|
rc.containerSpec.Volumes = []string{"claimed:/workspace/1"}
|
||||||
_, gotmount = rc.GetBindsAndMounts()
|
_, gotmount = rc.GetBindsAndMounts(t.Context())
|
||||||
assert.Equal(t, "/workspace/1/owner/repo", gotmount[rc.jobContainerName()])
|
assert.Equal(t, "/workspace/1/owner/repo", gotmount[rc.jobContainerName()])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1272,12 +1274,13 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, testcase := range []struct {
|
for _, testcase := range []struct {
|
||||||
name, health string
|
name, health string
|
||||||
pullError error
|
pullError, startError error
|
||||||
}{
|
}{
|
||||||
{name: "healthy", health: container.HealthHealthy},
|
{name: "healthy", health: container.HealthHealthy},
|
||||||
{name: "unhealthy", health: container.HealthUnhealthy},
|
{name: "unhealthy", health: container.HealthUnhealthy},
|
||||||
{name: "pull failure", pullError: errors.New("pull failed")},
|
{name: "pull failure", pullError: errors.New("pull failed")},
|
||||||
|
{name: "start failure", startError: errors.New("start failed")},
|
||||||
} {
|
} {
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
var operations []string
|
var operations []string
|
||||||
@@ -1291,7 +1294,7 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
for name, instance := range map[string]*containerMock{"job": job, "service": service} {
|
for name, instance := range map[string]*containerMock{"job": job, "service": service} {
|
||||||
instance.On("Pull", true).Return(record(name+".Pull", map[string]error{"job": testcase.pullError}[name]))
|
instance.On("Pull", true).Return(record(name+".Pull", map[string]error{"job": testcase.pullError}[name]))
|
||||||
instance.On("Create", mock.Anything, mock.Anything).Return(record(name+".Create", nil))
|
instance.On("Create", mock.Anything, mock.Anything).Return(record(name+".Create", nil))
|
||||||
instance.On("Start", false).Return(record(name+".Start", nil))
|
instance.On("Start", false).Return(record(name+".Start", map[string]error{"service": testcase.startError}[name]))
|
||||||
instance.On("Remove").Return(record(name+".Remove", nil))
|
instance.On("Remove").Return(record(name+".Remove", nil))
|
||||||
instance.On("Close").Return(record(name+".Close", nil))
|
instance.On("Close").Return(record(name+".Close", nil))
|
||||||
}
|
}
|
||||||
@@ -1302,10 +1305,14 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
service.On("Inspect", mock.Anything).Return(&container.Info{State: "running", Health: health}, nil).
|
service.On("Inspect", mock.Anything).Return(&container.Info{State: "running", Health: health}, nil).
|
||||||
Run(func(mock.Arguments) { operations = append(operations, "service.Inspect") }).Once()
|
Run(func(mock.Arguments) { operations = append(operations, "service.Inspect") }).Once()
|
||||||
}
|
}
|
||||||
service.On("DumpLogs", mock.Anything).Return(nil).
|
stdouts := map[string]io.Writer{}
|
||||||
Run(func(mock.Arguments) { operations = append(operations, "service.DumpLogs") })
|
service.On("DumpLogs", mock.Anything).Return(nil).Run(func(mock.Arguments) {
|
||||||
|
operations = append(operations, "service.DumpLogs")
|
||||||
|
fmt.Fprintln(stdouts["postgres:latest"], "::add-mask::leak")
|
||||||
|
})
|
||||||
origNewContainer := newContainer
|
origNewContainer := newContainer
|
||||||
newContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
|
newContainer = func(input *container.NewContainerInput) container.ExecutionsEnvironment {
|
||||||
|
stdouts[input.Image] = input.Stdout
|
||||||
return map[string]*containerMock{"postgres:latest": service, "node:20": job}[input.Image]
|
return map[string]*containerMock{"postgres:latest": service, "node:20": job}[input.Image]
|
||||||
}
|
}
|
||||||
t.Cleanup(func() { newContainer = origNewContainer })
|
t.Cleanup(func() { newContainer = origNewContainer })
|
||||||
@@ -1316,22 +1323,30 @@ func TestRunContext_cleanupFailedStart(t *testing.T) {
|
|||||||
Run: &model.Run{JobID: "job", Workflow: workflow},
|
Run: &model.Run{JobID: "job", Workflow: workflow},
|
||||||
platformImage: "node:20",
|
platformImage: "node:20",
|
||||||
}
|
}
|
||||||
ctx := common.WithDryrun(t.Context(), true)
|
logger, hook := logrustest.NewNullLogger()
|
||||||
|
ctx := common.WithLogger(common.WithDryrun(t.Context(), true), logger)
|
||||||
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
|
||||||
err = rc.startContainer().Then(rc.stopContainer()).Finally(rc.closeContainer())(ctx)
|
err = rc.startContainer().Then(rc.stopContainer()).Finally(rc.closeContainer())(ctx)
|
||||||
want := "job.Remove service.Remove service.Close service.Pull job.Pull"
|
want := "job.Remove service.Remove service.Close service.Pull job.Pull"
|
||||||
if testcase.pullError != nil {
|
switch {
|
||||||
|
case testcase.pullError != nil:
|
||||||
require.ErrorIs(t, err, testcase.pullError)
|
require.ErrorIs(t, err, testcase.pullError)
|
||||||
} else {
|
case testcase.startError != nil:
|
||||||
want += " service.Create service.Start service.Inspect job.Create job.Start job.Inspect job.Copy service.Inspect"
|
require.ErrorIs(t, err, testcase.startError)
|
||||||
|
want += " service.Create service.Start service.DumpLogs"
|
||||||
|
default:
|
||||||
|
want += " service.Create service.Start service.Inspect job.Create job.Start job.Inspect job.Copy service.Inspect service.DumpLogs"
|
||||||
if testcase.health == container.HealthUnhealthy {
|
if testcase.health == container.HealthUnhealthy {
|
||||||
require.ErrorContains(t, err, "the service 'postgres' is unhealthy")
|
require.ErrorContains(t, err, "the service 'postgres' is unhealthy")
|
||||||
want += " service.DumpLogs"
|
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.Equal(t, strings.Fields(want+" job.Remove service.Remove service.Close job.Close"), operations)
|
assert.Equal(t, strings.Fields(want+" job.Remove service.Remove service.Close job.Close"), operations)
|
||||||
|
assert.Empty(t, rc.Masks)
|
||||||
|
assert.Equal(t, testcase.pullError == nil && testcase.health != container.HealthUnhealthy, slices.ContainsFunc(hook.AllEntries(), func(entry *log.Entry) bool {
|
||||||
|
return strings.HasPrefix(entry.Message, "::group::Print service container logs")
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1587,12 +1602,39 @@ func TestImageOSFromImage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolvePlatformImageAppliesImagelessContainerToRunsOnImage(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, image string
|
||||||
|
want model.ContainerSpec
|
||||||
|
}{
|
||||||
|
{"without image key", "", model.ContainerSpec{Volumes: []string{"/host/data:/data"}, Env: map[string]string{"KEY": "value"}}},
|
||||||
|
{"with empty image", "image: '', ", model.ContainerSpec{}},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
rc := createIfTestRunContext(map[string]*model.Job{
|
||||||
|
"job1": createJob(t, "runs-on: toolkit\ncontainer: {"+tc.image+"volumes: [/host/data:/data], env: {KEY: value}}", ""),
|
||||||
|
})
|
||||||
|
require.NoError(t, rc.resolvePlatformImage(t.Context()))
|
||||||
|
require.NoError(t, evaluateJobEnvAndDefaults(t.Context(), rc))
|
||||||
|
|
||||||
|
assert.Equal(t, "ubuntu-latest", rc.platformImage)
|
||||||
|
assert.Equal(t, tc.want, rc.containerSpec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createRunsOnRunContext(t *testing.T, runsOn string) *RunContext {
|
func createRunsOnRunContext(t *testing.T, runsOn string) *RunContext {
|
||||||
return createIfTestRunContext(map[string]*model.Job{
|
return createIfTestRunContext(map[string]*model.Job{
|
||||||
"job1": createJob(t, "runs-on: "+runsOn, ""),
|
"job1": createJob(t, "runs-on: "+runsOn, ""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunContextContainerImageCannotSelectHost(t *testing.T) {
|
||||||
|
rc := createIfTestRunContext(map[string]*model.Job{"job1": createJob(t, "container: -self-hosted", "")})
|
||||||
|
require.NoError(t, rc.resolvePlatformImage(t.Context()))
|
||||||
|
assert.False(t, rc.IsHostEnv())
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunContextImageOS(t *testing.T) {
|
func TestRunContextImageOS(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type Config struct {
|
|||||||
NoActionPatch bool // run actions exactly as published, applying no compatibility patches, see patch_actions.go
|
NoActionPatch bool // run actions exactly as published, applying no compatibility patches, see patch_actions.go
|
||||||
|
|
||||||
PresetGitHubContext *model.GithubContext // overrides actor, ref, repository, token and related context fields
|
PresetGitHubContext *model.GithubContext // overrides actor, ref, repository, token and related context fields
|
||||||
|
WorkflowCallInputs map[string]any // the `inputs` Gitea resolved for a called workflow's job, nil for other jobs
|
||||||
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
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ func testPayloads(t *testing.T) {
|
|||||||
"cell-a-2",
|
"cell-a-2",
|
||||||
"cell-b-1",
|
"cell-b-1",
|
||||||
"cell-b-2",
|
"cell-b-2",
|
||||||
|
"imageless env=imageless-env-value host=imageless-host",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(logs, want) {
|
if !strings.Contains(logs, want) {
|
||||||
t.Errorf("stored logs are missing %q", want)
|
t.Errorf("stored logs are missing %q", want)
|
||||||
|
|||||||
Vendored
+5
@@ -3,6 +3,10 @@ on: push
|
|||||||
jobs:
|
jobs:
|
||||||
verify:
|
verify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
env:
|
||||||
|
IMAGELESS_ENV: imageless-env-value
|
||||||
|
options: --hostname imageless-host
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
image: ${{ vars.E2E_SERVICE_IMAGE }}
|
image: ${{ vars.E2E_SERVICE_IMAGE }}
|
||||||
@@ -19,6 +23,7 @@ jobs:
|
|||||||
echo '::notice::encoded-first%0Aencoded-second'
|
echo '::notice::encoded-first%0Aencoded-second'
|
||||||
echo "the variable is ${{ vars.GREETING }}"
|
echo "the variable is ${{ vars.GREETING }}"
|
||||||
echo "the secret is ${{ secrets.FOO }}"
|
echo "the secret is ${{ secrets.FOO }}"
|
||||||
|
echo "imageless env=$IMAGELESS_ENV host=$(hostname)"
|
||||||
curl --connect-timeout 1 --max-time 2 --retry 30 --retry-delay 1 --retry-max-time 30 --retry-all-errors -fsS -o /dev/null http://web/
|
curl --connect-timeout 1 --max-time 2 --retry 30 --retry-delay 1 --retry-max-time 30 --retry-all-errors -fsS -o /dev/null http://web/
|
||||||
produce:
|
produce:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
dario.cat/mergo v1.0.2
|
dario.cat/mergo v1.0.2
|
||||||
gitea.dev/actionslib v1.2.1
|
gitea.dev/actionslib v1.2.1
|
||||||
github.com/avast/retry-go/v5 v5.0.0
|
github.com/avast/retry-go/v5 v5.0.0
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0
|
github.com/bmatcuk/doublestar/v4 v4.10.2
|
||||||
github.com/containerd/errdefs v1.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
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
|||||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
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.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
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/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
github.com/cevatbarisyilmaz/ara v0.0.4 h1:SGH10hXpBJhhTlObuZzTuFn1rrdmjQImITXnZVPSodc=
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ func loadConfigCmd(configFile *string) *cobra.Command {
|
|||||||
|
|
||||||
configCmd.AddCommand(loadGenerateConfigCmd("generate"))
|
configCmd.AddCommand(loadGenerateConfigCmd("generate"))
|
||||||
configCmd.AddCommand(loadInitConfigCmd(configFile))
|
configCmd.AddCommand(loadInitConfigCmd(configFile))
|
||||||
|
configCmd.AddCommand(&cobra.Command{
|
||||||
|
Use: "runner-file",
|
||||||
|
Short: "Print the registration file path used by the runner",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
cfg, err := config.LoadDefault(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintln(cmd.OutOrStdout(), cfg.Runner.File)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
configCmd.AddCommand(&cobra.Command{
|
configCmd.AddCommand(&cobra.Command{
|
||||||
Use: "get <key>",
|
Use: "get <key>",
|
||||||
|
|||||||
@@ -97,3 +97,14 @@ func TestConfigCmdResolvesTheConfigFile(t *testing.T) {
|
|||||||
assert.Contains(t, err.Error(), "--config")
|
assert.Contains(t, err.Error(), "--config")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigCmdRunnerFileUsesConfiguredPathOrDefault(t *testing.T) {
|
||||||
|
out, _, err := runConfigCmd(t, "", "runner-file")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, ".runner\n", out)
|
||||||
|
file := filepath.Join(t.TempDir(), "config.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(file, []byte("runner:\n file: 'state dir/registration.json'\n"), 0o600))
|
||||||
|
out, _, err = runConfigCmd(t, file, "runner-file")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "state dir/registration.json\n", out)
|
||||||
|
}
|
||||||
@@ -350,6 +350,25 @@ func (r *Runner) cloneEnvs() map[string]string {
|
|||||||
return envs
|
return envs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restoreCalledWorkflowContext undoes Gitea's rewrite of `event_name` and `event.inputs` for older runners,
|
||||||
|
// and returns the job's `inputs` from `gitea_workflow_call`, nil if it is not a called workflow's job.
|
||||||
|
// See https://github.com/go-gitea/gitea/pull/39452
|
||||||
|
func restoreCalledWorkflowContext(task *runnerv1.Task) map[string]any {
|
||||||
|
workflowCall := task.GetContext().GetFields()["gitea_workflow_call"].GetStructValue().GetFields()
|
||||||
|
if workflowCall == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
task.Context.Fields["event_name"] = workflowCall["original_event_name"]
|
||||||
|
if event := task.Context.Fields["event"].GetStructValue(); event != nil {
|
||||||
|
if inputs, ok := workflowCall["original_event_inputs"]; ok {
|
||||||
|
event.Fields["inputs"] = inputs
|
||||||
|
} else {
|
||||||
|
delete(event.Fields, "inputs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return workflowCall["inputs"].GetStructValue().AsMap()
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -398,6 +417,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
workflowCallInputs := restoreCalledWorkflowContext(task)
|
||||||
r.reportSetup(reporter, task)
|
r.reportSetup(reporter, task)
|
||||||
|
|
||||||
workflow, jobID, err := generateWorkflow(task)
|
workflow, jobID, err := generateWorkflow(task)
|
||||||
@@ -536,6 +556,7 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
NoSkipCheckout: true,
|
NoSkipCheckout: true,
|
||||||
DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv,
|
DisableActEnv: r.cfg.Runner.SetActEnv != nil && !*r.cfg.Runner.SetActEnv,
|
||||||
PresetGitHubContext: preset,
|
PresetGitHubContext: preset,
|
||||||
|
WorkflowCallInputs: workflowCallInputs,
|
||||||
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,
|
||||||
|
|||||||
@@ -358,3 +358,48 @@ func TestNewRunnerCacheServiceV2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRestoreCalledWorkflowContext(t *testing.T) {
|
||||||
|
newTask := func(workflowCall map[string]any) *runnerv1.Task {
|
||||||
|
values := map[string]any{
|
||||||
|
"event_name": "workflow_call",
|
||||||
|
"event": map[string]any{"ref": "refs/heads/main", "inputs": map[string]any{"env": "leaf"}},
|
||||||
|
}
|
||||||
|
if workflowCall != nil {
|
||||||
|
values["gitea_workflow_call"] = workflowCall
|
||||||
|
}
|
||||||
|
taskCtx, err := structpb.NewStruct(values)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return &runnerv1.Task{Context: taskCtx}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("dispatched run", func(t *testing.T) {
|
||||||
|
task := newTask(map[string]any{
|
||||||
|
"original_event_name": "workflow_dispatch",
|
||||||
|
"original_event_inputs": map[string]any{"target": "prod"},
|
||||||
|
"inputs": map[string]any{"target": "prod", "env": "leaf"},
|
||||||
|
})
|
||||||
|
assert.Equal(t, map[string]any{"target": "prod", "env": "leaf"}, restoreCalledWorkflowContext(task))
|
||||||
|
taskCtx := task.Context.AsMap()
|
||||||
|
assert.Equal(t, "workflow_dispatch", taskCtx["event_name"])
|
||||||
|
assert.Equal(t, map[string]any{"ref": "refs/heads/main", "inputs": map[string]any{"target": "prod"}}, taskCtx["event"])
|
||||||
|
assert.Equal(t, []string{"::group::Inputs", "env: leaf", "target: prod", "::endgroup::"}, inputLines(task.Context.Fields))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("event without inputs", func(t *testing.T) {
|
||||||
|
task := newTask(map[string]any{
|
||||||
|
"original_event_name": "push",
|
||||||
|
"inputs": map[string]any{"env": "leaf"},
|
||||||
|
})
|
||||||
|
assert.Equal(t, map[string]any{"env": "leaf"}, restoreCalledWorkflowContext(task))
|
||||||
|
taskCtx := task.Context.AsMap()
|
||||||
|
assert.Equal(t, "push", taskCtx["event_name"])
|
||||||
|
assert.Equal(t, map[string]any{"ref": "refs/heads/main"}, taskCtx["event"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not a called workflow's job", func(t *testing.T) {
|
||||||
|
task := newTask(nil)
|
||||||
|
assert.Nil(t, restoreCalledWorkflowContext(task))
|
||||||
|
assert.Equal(t, "workflow_call", task.Context.AsMap()["event_name"])
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -56,10 +56,15 @@ func (r *Runner) setupLines(task *runnerv1.Task) []string {
|
|||||||
return append(lines, "::endgroup::")
|
return append(lines, "::endgroup::")
|
||||||
}
|
}
|
||||||
|
|
||||||
// inputLines lists the inputs the run was triggered with, as workflow_dispatch and workflow_call
|
// inputLines lists the inputs of the job: those Gitea resolved for a called workflow's job,
|
||||||
// carry them in the event payload. Empty when the event has none.
|
// or the ones a workflow_dispatch event carries in its payload. Empty when there are none.
|
||||||
func inputLines(fields map[string]*structpb.Value) []string {
|
func inputLines(fields map[string]*structpb.Value) []string {
|
||||||
inputs := fields["event"].GetStructValue().GetFields()["inputs"].GetStructValue().GetFields()
|
inputs := fields["event"].GetStructValue().GetFields()["inputs"].GetStructValue().GetFields()
|
||||||
|
// For a called workflow's job, restore its inputs from `gitea_workflow_call`.
|
||||||
|
// See https://github.com/go-gitea/gitea/pull/39452.
|
||||||
|
if workflowCall := fields["gitea_workflow_call"].GetStructValue(); workflowCall != nil {
|
||||||
|
inputs = workflowCall.GetFields()["inputs"].GetStructValue().GetFields()
|
||||||
|
}
|
||||||
if len(inputs) == 0 {
|
if len(inputs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -6,12 +6,11 @@ fi
|
|||||||
|
|
||||||
cd /data
|
cd /data
|
||||||
|
|
||||||
RUNNER_STATE_FILE=${RUNNER_STATE_FILE:-'.runner'}
|
|
||||||
|
|
||||||
CONFIG_ARG=""
|
CONFIG_ARG=""
|
||||||
if [[ ! -z "${CONFIG_FILE}" ]]; then
|
if [[ ! -z "${CONFIG_FILE}" ]]; then
|
||||||
CONFIG_ARG="--config ${CONFIG_FILE}"
|
CONFIG_ARG="--config ${CONFIG_FILE}"
|
||||||
fi
|
fi
|
||||||
|
RUNNER_STATE_FILE=${RUNNER_STATE_FILE:-$(gitea-runner ${CONFIG_ARG} config runner-file)}
|
||||||
LABEL_ARGS=""
|
LABEL_ARGS=""
|
||||||
if [[ ! -z "${GITEA_RUNNER_LABELS}" ]]; then
|
if [[ ! -z "${GITEA_RUNNER_LABELS}" ]]; then
|
||||||
LABEL_ARGS="--labels ${GITEA_RUNNER_LABELS}"
|
LABEL_ARGS="--labels ${GITEA_RUNNER_LABELS}"
|
||||||
|
|||||||
Reference in new issue
Block a user