1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-09-15 18:57:01 +00:00

fix: image credentials for services must not override container image credentials (#181)

- do not override username and password when looping over services
- split prepareJobContainer out of startJobContainer
- split getNetworkName out as it is used by both
- add unit tests for prepareJobContainer
- make containre.NewContainer mockable
- add MockVariable helper

Closes forgejo/runner#575

---

Note to reviewers: do not show whitespace change, the refactor will show in  a minimal way. When the fix is reverted the tests fail as follows:

```
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -81,4 +81,4 @@
        	            	   Image: (string) (len=10) "some:image",
        	            	-  Username: (string) (len=17) "containerusername",
        	            	-  Password: (string) (len=17) "containerpassword",
        	            	+  Username: (string) (len=16) "service2username",
        	            	+  Password: (string) (len=16) "service2password",
        	            	   Entrypoint: ([]string) (len=3) {
        	Test:       	TestStartJobContainer/Overlapping
```

Reviewed-on: https://code.forgejo.org/forgejo/act/pulls/181
Reviewed-by: Michael Kriese <michael.kriese@gmx.de>
Co-authored-by: Earl Warren <contact@earl-warren.org>
Co-committed-by: Earl Warren <contact@earl-warren.org>
This commit is contained in:
Earl Warren 2025-07-15 20:55:38 +00:00 committed by earl-warren
parent 6620cc1d18
commit c2f91f63df
4 changed files with 346 additions and 151 deletions

View file

@ -43,7 +43,7 @@ import (
)
// NewContainer creates a reference to a container
func NewContainer(input *NewContainerInput) ExecutionsEnvironment {
var NewContainer = func(input *NewContainerInput) ExecutionsEnvironment {
cr := new(containerReference)
cr.input = input
cr.toolCache = input.ToolCache

View file

@ -391,8 +391,18 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
}
}
func (rc *RunContext) startJobContainer() common.Executor {
return func(ctx context.Context) error {
func (rc *RunContext) getNetworkName(_ context.Context) (networkName string, createAndDeleteNetwork bool) {
// specify the network to which the container will connect when `docker create` stage. (like execute command line: docker create --network <networkName> <image>)
networkName = string(rc.Config.ContainerNetworkMode)
if networkName == "" {
// if networkName is empty string, will create a new network for the containers.
// and it will be removed after at last.
networkName, createAndDeleteNetwork = rc.networkName()
}
return networkName, createAndDeleteNetwork
}
func (rc *RunContext) prepareJobContainer(ctx context.Context) error {
logger := common.Logger(ctx)
image := rc.platformImage(ctx)
rawLogger := logger.WithField("raw_output", true)
@ -427,15 +437,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
ext := container.LinuxContainerEnvironmentExtensions{}
binds, mounts := rc.GetBindsAndMounts()
// specify the network to which the container will connect when `docker create` stage. (like execute command line: docker create --network <networkName> <image>)
networkName := string(rc.Config.ContainerNetworkMode)
var createAndDeleteNetwork bool
if networkName == "" {
// if networkName is empty string, will create a new network for the containers.
// and it will be removed after at last.
networkName, createAndDeleteNetwork = rc.networkName()
}
networkName, createAndDeleteNetwork := rc.getNetworkName(ctx)
// add service containers
for serviceID, spec := range rc.Run.Job().Services {
// interpolate env
@ -452,7 +454,7 @@ func (rc *RunContext) startJobContainer() common.Executor {
interpolatedCmd = append(interpolatedCmd, rc.ExprEval.Interpolate(ctx, v))
}
username, password, err = rc.handleServiceCredentials(ctx, spec.Credentials)
username, password, err := rc.handleServiceCredentials(ctx, spec.Credentials)
if err != nil {
return fmt.Errorf("failed to handle service %s credentials: %w", serviceID, err)
}
@ -562,6 +564,15 @@ func (rc *RunContext) startJobContainer() common.Executor {
return errors.New("Failed to create job container")
}
return nil
}
func (rc *RunContext) startJobContainer() common.Executor {
return func(ctx context.Context) error {
if err := rc.prepareJobContainer(ctx); err != nil {
return err
}
networkName, _ := rc.getNetworkName(ctx)
networkConfig := network.CreateOptions{
Driver: "bridge",
Scope: "local",

View file

@ -1,20 +1,26 @@
package runner
import (
"cmp"
"context"
"fmt"
"os"
"regexp"
"runtime"
"slices"
"sort"
"strings"
"testing"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/exprparser"
"github.com/nektos/act/pkg/model"
"github.com/nektos/act/pkg/testutils"
"github.com/docker/go-connections/nat"
log "github.com/sirupsen/logrus"
assert "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
yaml "gopkg.in/yaml.v3"
)
@ -704,3 +710,171 @@ func Test_createSimpleContainerName(t *testing.T) {
})
}
}
func TestPrepareJobContainer(t *testing.T) {
yaml := `
on:
push:
jobs:
job:
runs-on: docker
container:
image: some:image
credentials:
username: containerusername
password: containerpassword
services:
service1:
image: service1:image
credentials:
username: service1username
password: service1password
service2:
image: service2:image
credentials:
username: service2username
password: service2password
steps:
- run: echo ok
`
workflow, err := model.ReadWorkflow(strings.NewReader(yaml))
require.NoError(t, err)
testCases := []struct {
name string
step actionStep
inputs []container.NewContainerInput
}{
{
name: "Overlapping",
step: &stepActionRemote{
Step: &model.Step{
Uses: "org/repo/path@ref",
},
RunContext: &RunContext{
Config: &Config{
Workdir: "/my/workdir",
},
Run: &model.Run{
JobID: "job",
Workflow: workflow,
},
},
env: map[string]string{},
},
inputs: []container.NewContainerInput{
{
Name: "WORKFLOW-JOB-service1-24d1b6963554cd6e1a2f9bfcd21b822bf5b42547db24667196ac45f89072fdd9",
Image: "service1:image",
Username: "service1username",
Password: "service1password",
Entrypoint: nil,
Cmd: []string{},
WorkingDir: "",
Env: []string{},
ToolCache: "/opt/hostedtoolcache",
Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"},
Mounts: map[string]string{},
NetworkMode: "WORKFLOW_JOB-job-network",
Privileged: false,
UsernsMode: "",
Platform: "",
NetworkAliases: []string{"service1"},
ExposedPorts: nat.PortSet{},
PortBindings: nat.PortMap{},
ConfigOptions: "",
JobOptions: "",
AutoRemove: false,
ValidVolumes: []string{
"WORKFLOW_JOB",
"WORKFLOW_JOB-env",
"/var/run/docker.sock",
},
},
{
Name: "WORKFLOW-JOB-service2-7137cecabbdb942ae7bbfc8953de8f2a68e8dc9c92ad98cd6d095481b216f979",
Image: "service2:image",
Username: "service2username",
Password: "service2password",
Entrypoint: nil,
Cmd: []string{},
WorkingDir: "",
Env: []string{},
ToolCache: "/opt/hostedtoolcache",
Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"},
Mounts: map[string]string{},
NetworkMode: "WORKFLOW_JOB-job-network",
Privileged: false,
UsernsMode: "",
Platform: "",
NetworkAliases: []string{"service2"},
ExposedPorts: nat.PortSet{},
PortBindings: nat.PortMap{},
ConfigOptions: "",
JobOptions: "",
AutoRemove: false,
ValidVolumes: []string{
"WORKFLOW_JOB",
"WORKFLOW_JOB-env",
"/var/run/docker.sock",
},
},
{
Name: "WORKFLOW_JOB",
Image: "some:image",
Username: "containerusername",
Password: "containerpassword",
Entrypoint: []string{"tail", "-f", "/dev/null"},
Cmd: nil,
WorkingDir: "/my/workdir",
Env: []string{},
ToolCache: "/opt/hostedtoolcache",
Binds: []string{"/var/run/docker.sock:/var/run/docker.sock"},
Mounts: map[string]string{
"WORKFLOW_JOB": "/my/workdir",
"WORKFLOW_JOB-env": "/var/run/act",
},
NetworkMode: "WORKFLOW_JOB-job-network",
Privileged: false,
UsernsMode: "",
Platform: "",
NetworkAliases: []string{""},
ExposedPorts: nil,
PortBindings: nil,
ConfigOptions: "",
JobOptions: "",
AutoRemove: false,
ValidVolumes: []string{
"WORKFLOW_JOB",
"WORKFLOW_JOB-env",
"/var/run/docker.sock",
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
containerInputs := make([]container.NewContainerInput, 0, 5)
newContainer := container.NewContainer
defer testutils.MockVariable(&container.NewContainer, func(input *container.NewContainerInput) container.ExecutionsEnvironment {
c := *input
c.Stdout = nil
c.Stderr = nil
c.Env = []string{}
containerInputs = append(containerInputs, c)
return newContainer(input)
})()
ctx := context.Background()
rc := testCase.step.getRunContext()
rc.ExprEval = rc.NewExpressionEvaluator(ctx)
require.NoError(t, rc.prepareJobContainer(ctx))
slices.SortFunc(containerInputs, func(a, b container.NewContainerInput) int { return cmp.Compare(a.Name, b.Name) })
assert.EqualValues(t, testCase.inputs, containerInputs)
})
}
}

View file

@ -0,0 +1,10 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package testutils
func MockVariable[T any](p *T, v T) (reset func()) {
old := *p
*p = v
return func() { *p = old }
}