Merge pull request '[FORGEJO] add the create-runner-file subcommand' (#48) from earl-warren/runner:wip-offline into main
Reviewed-on: https://code.forgejo.org/forgejo/runner/pulls/48
This commit is contained in:
commit
ae015e2ce7
8 changed files with 329 additions and 12 deletions
|
@ -4,21 +4,52 @@ on:
|
||||||
- push
|
- push
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
FORGEJO_HOST_PORT: 'forgejo:3000'
|
||||||
|
FORGEJO_ADMIN_USER: 'root'
|
||||||
|
FORGEJO_ADMIN_PASSWORD: 'admin1234'
|
||||||
|
FORGEJO_RUNNER_SECRET: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
|
||||||
|
FORGEJO_SCRIPT: |
|
||||||
|
/bin/s6-svscan /etc/s6 & sleep 10 ; su -c "forgejo admin user create --admin --username $FORGEJO_ADMIN_USER --password $FORGEJO_ADMIN_PASSWORD --email root@example.com" git && su -c "forgejo forgejo-cli actions register --labels docker --name therunner --secret $FORGEJO_RUNNER_SECRET" git && sleep infinity
|
||||||
GOPROXY: https://goproxy.io,direct
|
GOPROXY: https://goproxy.io,direct
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
tests:
|
||||||
name: check and test
|
name: check and test
|
||||||
if: github.repository_owner != 'forgejo-integration' && github.repository_owner != 'forgejo-experimental' && github.repository_owner != 'forgejo-release'
|
if: github.repository_owner != 'forgejo-integration' && github.repository_owner != 'forgejo-experimental' && github.repository_owner != 'forgejo-release'
|
||||||
runs-on: ubuntu-latest
|
runs-on: docker
|
||||||
|
|
||||||
|
services:
|
||||||
|
forgejo:
|
||||||
|
image: codeberg.org/forgejo-integration/forgejo:1.20.0-4-rc2
|
||||||
|
env:
|
||||||
|
FORGEJO__security__INSTALL_LOCK: "true"
|
||||||
|
FORGEJO__log__LEVEL: "debug"
|
||||||
|
FORGEJO__actions__ENABLED: "true"
|
||||||
|
FORGEJO_ADMIN_USER: ${{ env.FORGEJO_ADMIN_USER }}
|
||||||
|
FORGEJO_ADMIN_PASSWORD: ${{ env.FORGEJO_ADMIN_PASSWORD }}
|
||||||
|
FORGEJO_RUNNER_SECRET: ${{ env.FORGEJO_RUNNER_SECRET }}
|
||||||
|
cmd:
|
||||||
|
- 'bash'
|
||||||
|
- '-c'
|
||||||
|
- ${{ env.FORGEJO_SCRIPT }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: 1.20
|
# pin because of https://github.com/nektos/act/issues/1908
|
||||||
|
go-version: 1.20.5
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: vet checks
|
|
||||||
run: make vet
|
- run: make vet
|
||||||
- name: build
|
|
||||||
run: make build
|
- run: make build
|
||||||
- name: test
|
|
||||||
run: make test
|
- name: check the forgejo server is responding
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y -qq jq curl
|
||||||
|
test $FORGEJO_ADMIN_USER = $(curl -sS http://$FORGEJO_ADMIN_USER:$FORGEJO_ADMIN_PASSWORD@$FORGEJO_HOST_PORT/api/v1/user | jq --raw-output .login)
|
||||||
|
|
||||||
|
- run: make FORGEJO_URL=http://$FORGEJO_HOST_PORT test
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -104,8 +104,7 @@ test: fmt-check
|
||||||
.PHONY: vet
|
.PHONY: vet
|
||||||
vet:
|
vet:
|
||||||
@echo "Running go vet..."
|
@echo "Running go vet..."
|
||||||
@$(GO) build code.gitea.io/gitea-vet
|
@$(GO) vet $(GO_PACKAGES_TO_VET)
|
||||||
@$(GO) vet -vettool=gitea-vet $(GO_PACKAGES_TO_VET)
|
|
||||||
|
|
||||||
install: $(GOFILES)
|
install: $(GOFILES)
|
||||||
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
$(GO) install -v -tags '$(TAGS)' -ldflags '$(EXTLDFLAGS)-s -w $(LDFLAGS)'
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/avast/retry-go/v4 v4.3.1
|
github.com/avast/retry-go/v4 v4.3.1
|
||||||
github.com/bufbuild/connect-go v1.3.1
|
github.com/bufbuild/connect-go v1.3.1
|
||||||
github.com/docker/docker v23.0.6+incompatible
|
github.com/docker/docker v23.0.6+incompatible
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mattn/go-isatty v0.0.18
|
github.com/mattn/go-isatty v0.0.18
|
||||||
github.com/nektos/act v0.0.0
|
github.com/nektos/act v0.0.0
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -84,6 +84,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
||||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
|
|
@ -41,6 +41,8 @@ func Execute(ctx context.Context) {
|
||||||
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated")
|
||||||
rootCmd.AddCommand(registerCmd)
|
rootCmd.AddCommand(registerCmd)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(createRunnerFileCmd(ctx, &configFile))
|
||||||
|
|
||||||
// ./act_runner daemon
|
// ./act_runner daemon
|
||||||
daemonCmd := &cobra.Command{
|
daemonCmd := &cobra.Command{
|
||||||
Use: "daemon",
|
Use: "daemon",
|
||||||
|
|
164
internal/app/cmd/create-runner-file.go
Normal file
164
internal/app/cmd/create-runner-file.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||||
|
"github.com/bufbuild/connect-go"
|
||||||
|
gouuid "github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"gitea.com/gitea/act_runner/internal/app/run"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createRunnerFileArgs struct {
|
||||||
|
Connect bool
|
||||||
|
InstanceAddr string
|
||||||
|
Secret string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func createRunnerFileCmd(ctx context.Context, configFile *string) *cobra.Command {
|
||||||
|
var argsVar createRunnerFileArgs
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create-runner-file",
|
||||||
|
Short: "Create a runner file using a shared secret used to pre-register the runner on the Forgejo instance",
|
||||||
|
Args: cobra.MaximumNArgs(0),
|
||||||
|
RunE: runCreateRunnerFile(ctx, &argsVar, configFile),
|
||||||
|
}
|
||||||
|
cmd.Flags().BoolVar(&argsVar.Connect, "connect", false, "tries to connect to the instance using the secret (Forgejo v1.21 instance or greater)")
|
||||||
|
cmd.Flags().StringVar(&argsVar.InstanceAddr, "instance", "", "Forgejo instance address")
|
||||||
|
cmd.MarkFlagRequired("instance")
|
||||||
|
cmd.Flags().StringVar(&argsVar.Secret, "secret", "", "secret shared with the Frogejo instance via forgejo-cli actions register")
|
||||||
|
cmd.MarkFlagRequired("secret")
|
||||||
|
cmd.Flags().StringVar(&argsVar.Name, "name", "", "Runner name")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// must be exactly the same as fogejo/models/actions/forgejo.go
|
||||||
|
func uuidFromSecret(secret string) (string, error) {
|
||||||
|
uuid, err := gouuid.FromBytes([]byte(secret[:16]))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("gouuid.FromBytes %v", err)
|
||||||
|
}
|
||||||
|
return uuid.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// should be exactly the same as forgejo/cmd/forgejo/actions.go
|
||||||
|
func validateSecret(secret string) error {
|
||||||
|
secretLen := len(secret)
|
||||||
|
if secretLen != 40 {
|
||||||
|
return fmt.Errorf("the secret must be exactly 40 characters long, not %d", secretLen)
|
||||||
|
}
|
||||||
|
if _, err := hex.DecodeString(secret); err != nil {
|
||||||
|
return fmt.Errorf("the secret must be an hexadecimal string: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ping(cfg *config.Config, reg *config.Registration) error {
|
||||||
|
// initial http client
|
||||||
|
cli := client.New(
|
||||||
|
reg.Address,
|
||||||
|
cfg.Runner.Insecure,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
ver.Version(),
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := cli.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{
|
||||||
|
Data: reg.UUID,
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ping %s failed %w", reg.Address, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCreateRunnerFile(ctx context.Context, args *createRunnerFileArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
|
||||||
|
return func(*cobra.Command, []string) error {
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
log.Info("Creating runner file")
|
||||||
|
|
||||||
|
//
|
||||||
|
// Prepare the registration data
|
||||||
|
//
|
||||||
|
cfg, err := config.LoadDefault(*configFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateSecret(args.Secret); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid, err := uuidFromSecret(args.Secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := args.Name
|
||||||
|
if name == "" {
|
||||||
|
name, _ = os.Hostname()
|
||||||
|
log.Infof("Runner name is empty, use hostname '%s'.", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := &config.Registration{
|
||||||
|
Name: name,
|
||||||
|
UUID: uuid,
|
||||||
|
Token: args.Secret,
|
||||||
|
Address: args.InstanceAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify the Forgejo instance is reachable
|
||||||
|
//
|
||||||
|
if err := ping(cfg, reg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Save the registration file
|
||||||
|
//
|
||||||
|
if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
|
||||||
|
return fmt.Errorf("failed to save runner config to %s: %w", cfg.Runner.File, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify the secret works
|
||||||
|
//
|
||||||
|
if args.Connect {
|
||||||
|
cli := client.New(
|
||||||
|
reg.Address,
|
||||||
|
cfg.Runner.Insecure,
|
||||||
|
reg.UUID,
|
||||||
|
reg.Token,
|
||||||
|
ver.Version(),
|
||||||
|
)
|
||||||
|
|
||||||
|
runner := run.NewRunner(cfg, reg, cli)
|
||||||
|
resp, err := runner.Declare(ctx, cfg.Runner.Labels)
|
||||||
|
|
||||||
|
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
|
||||||
|
log.Warn("Cannot verify the connection because the Forgejo instance is lower than v1.21")
|
||||||
|
} else if err != nil {
|
||||||
|
log.WithError(err).Error("fail to invoke Declare")
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
log.Infof("connection successful: %s, with version: %s, with labels: %v",
|
||||||
|
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
118
internal/app/cmd/create-runner-file_test.go
Normal file
118
internal/app/cmd/create-runner-file_test.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/client"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/config"
|
||||||
|
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
||||||
|
"github.com/bufbuild/connect-go"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func executeCommand(ctx context.Context, cmd *cobra.Command, args ...string) (string, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
cmd.SetOut(buf)
|
||||||
|
cmd.SetErr(buf)
|
||||||
|
cmd.SetArgs(args)
|
||||||
|
|
||||||
|
err := cmd.ExecuteContext(ctx)
|
||||||
|
|
||||||
|
return buf.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_createRunnerFileCmd(t *testing.T) {
|
||||||
|
configFile := "config.yml"
|
||||||
|
ctx := context.Background()
|
||||||
|
cmd := createRunnerFileCmd(ctx, &configFile)
|
||||||
|
output, err := executeCommand(ctx, cmd)
|
||||||
|
assert.ErrorContains(t, err, `required flag(s) "instance", "secret" not set`)
|
||||||
|
assert.Contains(t, output, "Usage:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_validateSecret(t *testing.T) {
|
||||||
|
assert.ErrorContains(t, validateSecret("abc"), "exactly 40 characters")
|
||||||
|
assert.ErrorContains(t, validateSecret("ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), "must be an hexadecimal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_uuidFromSecret(t *testing.T) {
|
||||||
|
uuid, err := uuidFromSecret("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, uuid, "41414141-4141-4141-4141-414141414141")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_ping(t *testing.T) {
|
||||||
|
cfg := &config.Config{}
|
||||||
|
address := os.Getenv("FORGEJO_URL")
|
||||||
|
if address == "" {
|
||||||
|
address = "https://code.forgejo.org"
|
||||||
|
}
|
||||||
|
reg := &config.Registration{
|
||||||
|
Address: address,
|
||||||
|
UUID: "create-runner-file_test.go",
|
||||||
|
}
|
||||||
|
assert.NoError(t, ping(cfg, reg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_runCreateRunnerFile(t *testing.T) {
|
||||||
|
//
|
||||||
|
// Set the .runner file to be in a temporary directory
|
||||||
|
//
|
||||||
|
dir := t.TempDir()
|
||||||
|
configFile := dir + "/config.yml"
|
||||||
|
runnerFile := dir + "/.runner"
|
||||||
|
cfg, err := config.LoadDefault("")
|
||||||
|
cfg.Runner.File = runnerFile
|
||||||
|
yamlData, err := yaml.Marshal(cfg)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, os.WriteFile(configFile, yamlData, 0o666))
|
||||||
|
|
||||||
|
instance, has := os.LookupEnv("FORGEJO_URL")
|
||||||
|
if !has {
|
||||||
|
instance = "https://code.forgejo.org"
|
||||||
|
}
|
||||||
|
secret, has := os.LookupEnv("FORGEJO_RUNNER_SECRET")
|
||||||
|
assert.True(t, has)
|
||||||
|
name := "testrunner"
|
||||||
|
|
||||||
|
//
|
||||||
|
// Run create-runner-file
|
||||||
|
//
|
||||||
|
ctx := context.Background()
|
||||||
|
cmd := createRunnerFileCmd(ctx, &configFile)
|
||||||
|
output, err := executeCommand(ctx, cmd, "--connect", "--secret", secret, "--instance", instance, "--name", name)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, "", output)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Read back the runner file and verify its content
|
||||||
|
//
|
||||||
|
reg, err := config.LoadRegistration(runnerFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, secret, reg.Token)
|
||||||
|
assert.EqualValues(t, instance, reg.Address)
|
||||||
|
|
||||||
|
//
|
||||||
|
// Verify that fetching a task successfully returns there is
|
||||||
|
// no task for this runner
|
||||||
|
//
|
||||||
|
cli := client.New(
|
||||||
|
reg.Address,
|
||||||
|
cfg.Runner.Insecure,
|
||||||
|
reg.UUID,
|
||||||
|
reg.Token,
|
||||||
|
ver.Version(),
|
||||||
|
)
|
||||||
|
resp, err := cli.FetchTask(ctx, connect.NewRequest(&runnerv1.FetchTaskRequest{}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, resp.Msg.Task)
|
||||||
|
}
|
|
@ -101,7 +101,7 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
|
||||||
resp, err := runner.Declare(ctx, ls.Names())
|
resp, err := runner.Declare(ctx, ls.Names())
|
||||||
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
|
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
|
||||||
// Gitea instance is older version. skip declare step.
|
// Gitea instance is older version. skip declare step.
|
||||||
log.Warn("Because the Gitea instance is an old version, skip declare labels and version.")
|
log.Warn("Because the Forgejo instance is an old version, skip declare labels and version.")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.WithError(err).Error("fail to invoke Declare")
|
log.WithError(err).Error("fail to invoke Declare")
|
||||||
return err
|
return err
|
||||||
|
|
Loading…
Reference in a new issue