forgejo-runner

git clone git://git.lin.moe/forgejo-runner.git

  1// SPDX-License-Identifier: MIT
  2
  3package cmd
  4
  5import (
  6	"context"
  7	"encoding/hex"
  8	"fmt"
  9	"os"
 10
 11	pingv1 "code.gitea.io/actions-proto-go/ping/v1"
 12	"connectrpc.com/connect"
 13	gouuid "github.com/google/uuid"
 14	log "github.com/sirupsen/logrus"
 15	"github.com/spf13/cobra"
 16
 17	"gitea.com/gitea/act_runner/internal/app/run"
 18	"gitea.com/gitea/act_runner/internal/pkg/client"
 19	"gitea.com/gitea/act_runner/internal/pkg/config"
 20	"gitea.com/gitea/act_runner/internal/pkg/ver"
 21)
 22
 23type createRunnerFileArgs struct {
 24	Connect      bool
 25	InstanceAddr string
 26	Secret       string
 27	Name         string
 28}
 29
 30func createRunnerFileCmd(ctx context.Context, configFile *string) *cobra.Command {
 31	var argsVar createRunnerFileArgs
 32	cmd := &cobra.Command{
 33		Use:   "create-runner-file",
 34		Short: "Create a runner file using a shared secret used to pre-register the runner on the Forgejo instance",
 35		Args:  cobra.MaximumNArgs(0),
 36		RunE:  runCreateRunnerFile(ctx, &argsVar, configFile),
 37	}
 38	cmd.Flags().BoolVar(&argsVar.Connect, "connect", false, "tries to connect to the instance using the secret (Forgejo v1.21 instance or greater)")
 39	cmd.Flags().StringVar(&argsVar.InstanceAddr, "instance", "", "Forgejo instance address")
 40	cmd.MarkFlagRequired("instance")
 41	cmd.Flags().StringVar(&argsVar.Secret, "secret", "", "secret shared with the Forgejo instance via forgejo-cli actions register")
 42	cmd.MarkFlagRequired("secret")
 43	cmd.Flags().StringVar(&argsVar.Name, "name", "", "Runner name")
 44
 45	return cmd
 46}
 47
 48// must be exactly the same as fogejo/models/actions/forgejo.go
 49func uuidFromSecret(secret string) (string, error) {
 50	uuid, err := gouuid.FromBytes([]byte(secret[:16]))
 51	if err != nil {
 52		return "", fmt.Errorf("gouuid.FromBytes %v", err)
 53	}
 54	return uuid.String(), nil
 55}
 56
 57// should be exactly the same as forgejo/cmd/forgejo/actions.go
 58func validateSecret(secret string) error {
 59	secretLen := len(secret)
 60	if secretLen != 40 {
 61		return fmt.Errorf("the secret must be exactly 40 characters long, not %d", secretLen)
 62	}
 63	if _, err := hex.DecodeString(secret); err != nil {
 64		return fmt.Errorf("the secret must be an hexadecimal string: %w", err)
 65	}
 66	return nil
 67}
 68
 69func ping(cfg *config.Config, reg *config.Registration) error {
 70	// initial http client
 71	cli := client.New(
 72		reg.Address,
 73		cfg.Runner.Insecure,
 74		"",
 75		"",
 76		ver.Version(),
 77	)
 78
 79	_, err := cli.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{
 80		Data: reg.UUID,
 81	}))
 82	if err != nil {
 83		return fmt.Errorf("ping %s failed %w", reg.Address, err)
 84	}
 85	return nil
 86}
 87
 88func runCreateRunnerFile(ctx context.Context, args *createRunnerFileArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
 89	return func(*cobra.Command, []string) error {
 90		log.SetLevel(log.DebugLevel)
 91		log.Info("Creating runner file")
 92
 93		//
 94		// Prepare the registration data
 95		//
 96		cfg, err := config.LoadDefault(*configFile)
 97		if err != nil {
 98			return fmt.Errorf("invalid configuration: %w", err)
 99		}
100
101		if err := validateSecret(args.Secret); err != nil {
102			return err
103		}
104
105		uuid, err := uuidFromSecret(args.Secret)
106		if err != nil {
107			return err
108		}
109
110		name := args.Name
111		if name == "" {
112			name, _ = os.Hostname()
113			log.Infof("Runner name is empty, use hostname '%s'.", name)
114		}
115
116		reg := &config.Registration{
117			Name:    name,
118			UUID:    uuid,
119			Token:   args.Secret,
120			Address: args.InstanceAddr,
121		}
122
123		//
124		// Verify the Forgejo instance is reachable
125		//
126		if err := ping(cfg, reg); err != nil {
127			return err
128		}
129
130		//
131		// Save the registration file
132		//
133		if err := config.SaveRegistration(cfg.Runner.File, reg); err != nil {
134			return fmt.Errorf("failed to save runner config to %s: %w", cfg.Runner.File, err)
135		}
136
137		//
138		// Verify the secret works
139		//
140		if args.Connect {
141			cli := client.New(
142				reg.Address,
143				cfg.Runner.Insecure,
144				reg.UUID,
145				reg.Token,
146				ver.Version(),
147			)
148
149			runner := run.NewRunner(cfg, reg, cli)
150			resp, err := runner.Declare(ctx, cfg.Runner.Labels)
151
152			if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented {
153				log.Warn("Cannot verify the connection because the Forgejo instance is lower than v1.21")
154			} else if err != nil {
155				log.WithError(err).Error("fail to invoke Declare")
156				return err
157			} else {
158				log.Infof("connection successful: %s, with version: %s, with labels: %v",
159					resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
160			}
161		}
162		return nil
163	}
164}