feat: honour GITEA_RUNNER_LABELS on daemon start and accept labels containing a colon (#1085)

Fixes #648
Fixes #656
Fixes #664

---------

Co-authored-by: silverwind <me@silverwind.io>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1085
Reviewed-by: silverwind <2021+silverwind@noreply.gitea.com>
This commit is contained in:
bircni
2026-07-15 16:58:07 +00:00
parent d6882b3df5
commit 58c5eb8d21
10 changed files with 146 additions and 28 deletions

View File

@@ -51,6 +51,7 @@ func Execute(ctx context.Context) {
RunE: runDaemon(ctx, &daemArgs, &configFile),
}
daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "Run one job then exit")
daemonCmd.Flags().StringVar(&daemArgs.Labels, "labels", os.Getenv("GITEA_RUNNER_LABELS"), "Runner labels, comma separated. Overrides the labels of an already registered runner")
rootCmd.AddCommand(daemonCmd)
// ./gitea-runner exec

View File

@@ -49,10 +49,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
return fmt.Errorf("failed to load registration file: %w", err)
}
lbls := reg.Labels
if len(cfg.Runner.Labels) > 0 {
lbls = cfg.Runner.Labels
}
lbls := resolveLabels(daemArgs.Labels, cfg.Runner.Labels, reg.Labels)
ls := labels.Labels{}
for _, l := range lbls {
@@ -203,7 +200,30 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}
type daemonArgs struct {
Once bool
Once bool
Labels string
}
// resolveLabels picks the labels to run with: --labels/GITEA_RUNNER_LABELS > config > .runner.
// The flag lets a registered runner change its labels without deleting the .runner file.
func resolveLabels(argLabels string, cfgLabels, regLabels []string) []string {
if lbls := splitLabels(argLabels); len(lbls) > 0 {
return lbls
}
if len(cfgLabels) > 0 {
return cfgLabels
}
return regLabels
}
func splitLabels(s string) []string {
var lbls []string
for l := range strings.SplitSeq(s, ",") {
if l = strings.TrimSpace(l); l != "" {
lbls = append(lbls, l)
}
}
return lbls
}
// initLogging setup the global logrus logger.

View File

@@ -12,6 +12,32 @@ import (
"github.com/stretchr/testify/require"
)
func TestResolveLabels(t *testing.T) {
var (
cfgLabels = []string{"cfg:host"}
regLabels = []string{"reg:host"}
)
tests := []struct {
name string
arg string
cfg []string
reg []string
want []string
}{
{"flag wins", "flag:host,other", cfgLabels, regLabels, []string{"flag:host", "other"}},
{"config wins over registration", "", cfgLabels, regLabels, cfgLabels},
{"registration is the fallback", "", nil, regLabels, regLabels},
{"blank flag is ignored", " , ", cfgLabels, regLabels, cfgLabels},
{"nothing configured", "", nil, nil, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, resolveLabels(tt.arg, tt.cfg, tt.reg))
})
}
}
func TestGetDockerSocketPathUsesConfigAndEnvironment(t *testing.T) {
got, err := getDockerSocketPath("tcp://docker.example:2376")
require.NoError(t, err)

View File

@@ -387,7 +387,10 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
ls := make([]string, len(reg.Labels))
for i, v := range reg.Labels {
l, _ := labels.Parse(v)
l, err := labels.Parse(v)
if err != nil {
return fmt.Errorf("failed to parse label %q: %w", v, err)
}
ls[i] = l.Name
}
// register new runner.

View File

@@ -15,11 +15,11 @@ import (
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
err := registerNoInteractive(t.Context(), "", &registerArgs{
Labels: "label:invalid",
Labels: "ubuntu:host,,broken",
Token: "token",
InstanceAddr: "http://localhost:3000",
})
assert.Error(t, err, "unsupported schema: invalid")
assert.ErrorContains(t, err, "empty label")
}
func TestRegisterInputsValidate(t *testing.T) {
@@ -40,8 +40,8 @@ func TestRegisterInputsValidate(t *testing.T) {
},
{
name: "invalid label",
inputs: registerInputs{InstanceAddr: "http://localhost:3000", Token: "token", Labels: []string{"ubuntu:vm:bad"}},
wantErr: "unsupported schema: vm",
inputs: registerInputs{InstanceAddr: "http://localhost:3000", Token: "token", Labels: []string{""}},
wantErr: "empty label",
},
{
name: "valid",
@@ -62,7 +62,9 @@ func TestRegisterInputsValidate(t *testing.T) {
func TestValidateLabels(t *testing.T) {
require.NoError(t, validateLabels([]string{"ubuntu:host", "ubuntu:docker://node:18"}))
require.Error(t, validateLabels([]string{"ubuntu:host", "ubuntu:vm:bad"}))
// a colon that is not a supported schema is part of the label name
require.NoError(t, validateLabels([]string{"pool:e57e18d4-10d4-406f-93bf-60f127221bdd"}))
require.Error(t, validateLabels([]string{"ubuntu:host", ""}))
}
func TestRegisterInputsStageValue(t *testing.T) {
@@ -106,11 +108,10 @@ func TestRegisterInputsAssignToNext(t *testing.T) {
t.Run("labels from config skip the labels stage", func(t *testing.T) {
cfg := &config.Config{}
cfg.Runner.Labels = []string{"ubuntu:host", "ubuntu:vm:bad"}
cfg.Runner.Labels = []string{"ubuntu:host", "", "pool:e57e18d4"}
inputs := &registerInputs{}
require.Equal(t, StageWaitingForRegistration, inputs.assignToNext(StageInputRunnerName, "runner", cfg))
// only the valid label survives
require.Equal(t, []string{"ubuntu:host"}, inputs.Labels)
require.Equal(t, []string{"ubuntu:host", "pool:e57e18d4"}, inputs.Labels)
})
t.Run("blank labels input uses defaults", func(t *testing.T) {
@@ -121,10 +122,16 @@ func TestRegisterInputsAssignToNext(t *testing.T) {
t.Run("invalid labels input loops back", func(t *testing.T) {
inputs := &registerInputs{}
require.Equal(t, StageInputLabels, inputs.assignToNext(StageInputLabels, "ubuntu:vm:bad", emptyCfg))
require.Equal(t, StageInputLabels, inputs.assignToNext(StageInputLabels, "ubuntu:host,,bad", emptyCfg))
require.Nil(t, inputs.Labels)
})
t.Run("labels containing a colon are accepted", func(t *testing.T) {
inputs := &registerInputs{}
require.Equal(t, StageWaitingForRegistration, inputs.assignToNext(StageInputLabels, "pool:e57e18d4,ubuntu:host", emptyCfg))
require.Equal(t, []string{"pool:e57e18d4", "ubuntu:host"}, inputs.Labels)
})
t.Run("overwrite local config", func(t *testing.T) {
inputs := &registerInputs{}
require.Equal(t, StageInputInstance, inputs.assignToNext(StageOverwriteLocalConfig, "Y", emptyCfg))