diff --git a/cmd/daemon.go b/cmd/daemon.go index fafeb04..28c2992 100644 --- a/cmd/daemon.go +++ b/cmd/daemon.go @@ -3,7 +3,12 @@ package cmd import ( "context" "os" - "strings" + + "github.com/joho/godotenv" + "github.com/mattn/go-isatty" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" "codeberg.org/forgejo/runner/artifactcache" "codeberg.org/forgejo/runner/client" @@ -11,12 +16,6 @@ import ( "codeberg.org/forgejo/runner/engine" "codeberg.org/forgejo/runner/poller" "codeberg.org/forgejo/runner/runtime" - - "github.com/joho/godotenv" - "github.com/mattn/go-isatty" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "golang.org/x/sync/errgroup" ) func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, args []string) error { @@ -35,8 +34,8 @@ func runDaemon(ctx context.Context, envFile string) func(cmd *cobra.Command, arg // require docker if a runner label uses a docker backend needsDocker := false for _, l := range cfg.Runner.Labels { - splits := strings.SplitN(l, ":", 2) - if len(splits) == 2 && strings.HasPrefix(splits[1], "docker://") { + _, schema, _, _ := runtime.ParseLabel(l) + if schema == "docker" { needsDocker = true break } diff --git a/cmd/register.go b/cmd/register.go index 7bd5662..1b58477 100644 --- a/cmd/register.go +++ b/cmd/register.go @@ -1,3 +1,6 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package cmd import ( @@ -6,14 +9,16 @@ import ( "fmt" "os" "os/signal" - "runtime" + goruntime "runtime" "strings" "time" pingv1 "code.gitea.io/actions-proto-go/ping/v1" + "codeberg.org/forgejo/runner/client" "codeberg.org/forgejo/runner/config" "codeberg.org/forgejo/runner/register" + "codeberg.org/forgejo/runner/runtime" "github.com/bufbuild/connect-go" "github.com/joho/godotenv" @@ -34,7 +39,7 @@ func runRegister(ctx context.Context, regArgs *registerArgs, envFile string) fun log.SetLevel(log.DebugLevel) log.Infof("Registering runner, arch=%s, os=%s, version=%s.", - runtime.GOARCH, runtime.GOOS, version) + goruntime.GOARCH, goruntime.GOOS, version) // runner always needs root permission if os.Getuid() != 0 { @@ -118,12 +123,9 @@ func (r *registerInputs) validate() error { func validateLabels(labels []string) error { for _, label := range labels { - values := strings.SplitN(label, ":", 2) - if len(values) > 2 { - return fmt.Errorf("Invalid label: %s", label) + if _, _, _, err := runtime.ParseLabel(label); err != nil { + return err } - // len(values) == 1, label for non docker execution environment - // TODO: validate value format, like docker://node:16-buster } return nil } @@ -164,7 +166,7 @@ func (r *registerInputs) assignToNext(stage registerStage, value string) registe } if validateLabels(r.CustomLabels) != nil { - log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster)") + log.Infoln("Invalid labels, please input again, leave blank to use the default labels (for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host)") return StageInputCustomLabels } return StageWaitingForRegistration @@ -221,14 +223,14 @@ func printStageHelp(stage registerStage) { case StageOverwriteLocalConfig: log.Infoln("Runner is already registered, overwrite local config? [y/N]") case StageInputInstance: - log.Infoln("Enter the Forgejo instance URL (for example, https://codeberg.org/):") + log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):") case StageInputToken: log.Infoln("Enter the runner token:") case StageInputRunnerName: hostname, _ := os.Hostname() - log.Infof("Enter the runner name (if set empty, use hostname:%s ):\n", hostname) + log.Infof("Enter the runner name (if set empty, use hostname: %s):\n", hostname) case StageInputCustomLabels: - log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, self-hosted,ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster):") + log.Infoln("Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster,linux_arm:host):") case StageWaitingForRegistration: log.Infoln("Waiting for registration...") } @@ -290,11 +292,11 @@ func doRegister(cfg *config.Config, inputs *registerInputs) error { } if err != nil { log.WithError(err). - Errorln("Cannot ping the Forgejo instance server") + Errorln("Cannot ping the Gitea instance server") // TODO: if ping failed, retry or exit time.Sleep(time.Second) } else { - log.Debugln("Successfully pinged the Forgejo instance server") + log.Debugln("Successfully pinged the Gitea instance server") break } } diff --git a/cmd/register_test.go b/cmd/register_test.go deleted file mode 100644 index 0179a97..0000000 --- a/cmd/register_test.go +++ /dev/null @@ -1,10 +0,0 @@ -package cmd - -import "testing" - -func TestValidateLabels(t *testing.T) { - labels := []string{"ubuntu-latest:docker://node:16-buster", "self-hosted"} - if err := validateLabels(labels); err != nil { - t.Errorf("validateLabels() error = %v", err) - } -} diff --git a/runtime/label.go b/runtime/label.go new file mode 100644 index 0000000..c7aa001 --- /dev/null +++ b/runtime/label.go @@ -0,0 +1,26 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runtime + +import ( + "fmt" + "strings" +) + +func ParseLabel(str string) (label, schema, arg string, err error) { + splits := strings.SplitN(str, ":", 3) + label = splits[0] + schema = "host" + arg = "" + if len(splits) >= 2 { + schema = splits[1] + } + if len(splits) >= 3 { + arg = splits[2] + } + if schema != "host" && schema != "docker" { + return "", "", "", fmt.Errorf("unsupported schema: %s", schema) + } + return +} diff --git a/runtime/label_test.go b/runtime/label_test.go new file mode 100644 index 0000000..2bd901a --- /dev/null +++ b/runtime/label_test.go @@ -0,0 +1,60 @@ +package runtime + +import "testing" + +func TestParseLabel(t *testing.T) { + tests := []struct { + args string + wantLabel string + wantSchema string + wantArg string + wantErr bool + }{ + { + args: "ubuntu:docker://node:18", + wantLabel: "ubuntu", + wantSchema: "docker", + wantArg: "//node:18", + wantErr: false, + }, + { + args: "ubuntu:host", + wantLabel: "ubuntu", + wantSchema: "host", + wantArg: "", + wantErr: false, + }, + { + args: "ubuntu", + wantLabel: "ubuntu", + wantSchema: "host", + wantArg: "", + wantErr: false, + }, + { + args: "ubuntu:vm:ubuntu-18.04", + wantLabel: "", + wantSchema: "", + wantArg: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.args, func(t *testing.T) { + gotLabel, gotSchema, gotArg, err := ParseLabel(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("parseLabel() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotLabel != tt.wantLabel { + t.Errorf("parseLabel() gotLabel = %v, want %v", gotLabel, tt.wantLabel) + } + if gotSchema != tt.wantSchema { + t.Errorf("parseLabel() gotSchema = %v, want %v", gotSchema, tt.wantSchema) + } + if gotArg != tt.wantArg { + t.Errorf("parseLabel() gotArg = %v, want %v", gotArg, tt.wantArg) + } + }) + } +} diff --git a/runtime/runtime.go b/runtime/runtime.go index c5a9118..2abf795 100644 --- a/runtime/runtime.go +++ b/runtime/runtime.go @@ -7,6 +7,7 @@ import ( runnerv1 "code.gitea.io/actions-proto-go/runner/v1" "codeberg.org/forgejo/runner/artifactcache" "codeberg.org/forgejo/runner/client" + log "github.com/sirupsen/logrus" ) // Runner runs the pipeline. @@ -31,28 +32,24 @@ func (s *Runner) Run(ctx context.Context, task *runnerv1.Task) error { } func (s *Runner) platformPicker(labels []string) string { - // "ubuntu-18.04:docker://node:16-buster" - // "self-hosted" - - platforms := make(map[string]string, len(labels)) + platforms := make(map[string]string, len(s.Labels)) for _, l := range s.Labels { - // "ubuntu-18.04:docker://node:16-buster" - splits := strings.SplitN(l, ":", 2) - if len(splits) == 1 { - // identifier for non docker execution environment - platforms[splits[0]] = "-self-hosted" + label, schema, arg, err := ParseLabel(l) + if err != nil { + log.Errorf("invaid label %q: %v", l, err) continue } - // ["ubuntu-18.04", "docker://node:16-buster"] - k, v := splits[0], splits[1] - if prefix := "docker://"; !strings.HasPrefix(v, prefix) { + switch schema { + case "docker": + // TODO "//" will be ignored, maybe we should use 'ubuntu-18.04:docker:node:16-buster' instead + platforms[label] = strings.TrimPrefix(arg, "//") + case "host": + platforms[label] = "-self-hosted" + default: + // It should not happen, because ParseLabel has checked it. continue - } else { - v = strings.TrimPrefix(v, prefix) } - // ubuntu-18.04 => node:16-buster - platforms[k] = v } for _, label := range labels { @@ -67,6 +64,8 @@ func (s *Runner) platformPicker(labels []string) string { // ["with-gpu"] => "linux:with-gpu" // ["ubuntu-22.04", "with-gpu"] => "ubuntu:22.04_with-gpu" - // return default - return "node:16-bullseye" + // return default. + // So the runner receives a task with a label that the runner doesn't have, + // it happens when the user have edited the label of the runner in the web UI. + return "node:16-bullseye" // TODO: it may be not correct, what if the runner is used as host mode only? }