[FORGEJO] add the create-runner-file

This commit is contained in:
Earl Warren 2023-07-11 22:45:49 +02:00
parent e6f0b3b5b8
commit 5a1ea04ce8
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
7 changed files with 328 additions and 10 deletions

View file

@ -4,21 +4,52 @@ on:
- push
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
jobs:
lint:
tests:
name: check and test
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:
- uses: actions/setup-go@v3
with:
go-version: 1.20
# pin because of https://github.com/nektos/act/issues/1908
go-version: 1.20.5
- uses: actions/checkout@v3
- name: vet checks
run: make vet
- name: build
run: make build
- name: test
run: make test
- run: make vet
- run: make build
- 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

1
go.mod
View file

@ -8,6 +8,7 @@ require (
github.com/avast/retry-go/v4 v4.5.0
github.com/bufbuild/connect-go v1.10.0
github.com/docker/docker v24.0.5+incompatible
github.com/google/uuid v1.3.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-isatty v0.0.19
github.com/nektos/act v0.2.49

2
go.sum
View file

@ -83,6 +83,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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
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.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=

View file

@ -41,6 +41,8 @@ func Execute(ctx context.Context) {
registerCmd.Flags().StringVar(&regArgs.Labels, "labels", "", "Runner tags, comma separated")
rootCmd.AddCommand(registerCmd)
rootCmd.AddCommand(createRunnerFileCmd(ctx, &configFile))
// ./act_runner daemon
daemonCmd := &cobra.Command{
Use: "daemon",

View 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
}
}

View 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)
}

View file

@ -101,7 +101,7 @@ func runDaemon(ctx context.Context, configFile *string) func(cmd *cobra.Command,
resp, err := runner.Declare(ctx, ls.Names())
if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
// 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 {
log.WithError(err).Error("fail to invoke Declare")
return err