Files
scaleset/examples/dockerscaleset/main.go
T
Nikola JokicandGitHub 83fb2e2837 Set user agent as soon as the scale set is created, and remove the proxy info (#8)
* Set user agent as soon as the scale set is created

* fix test
2025-10-31 14:16:02 +01:00

191 lines
5.5 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
"github.com/actions/scaleset"
"github.com/actions/scaleset/listener"
"github.com/docker/docker/api/types/image"
dockerclient "github.com/moby/moby/client"
"github.com/spf13/cobra"
)
func init() {
flags := cmd.Flags()
flags.StringVar(&cfg.RegistrationURL, "url", "", "REQUIRED: URL where to register your scale set (e.g. https://github.com/org/repo)")
flags.IntVar(&cfg.MaxRunners, "max-runners", 10, "Maximum number of runners")
flags.IntVar(&cfg.MinRunners, "min-runners", 0, "Minimum number of runners")
flags.StringVar(&cfg.ScaleSetName, "name", "", "REQUIRED: Name of your scale set")
flags.StringVar(&cfg.RunnerGroup, "runner-group", scaleset.DefaultRunnerGroup, "Name of the runner group your scale set should belong to")
flags.StringVar(&cfg.GitHubApp.ClientID, "app-client-id", "", "GitHub App client id")
flags.Int64Var(&cfg.GitHubApp.InstallationID, "app-installation-id", 0, "GitHub App installation ID")
flags.StringVar(&cfg.GitHubApp.PrivateKey, "app-private-key", "", "GitHub App private key")
flags.StringVar(&cfg.Token, "token", "", "Personal access token (can be used in place of a GitHub App, although not recommended)")
flags.StringVar(&cfg.LogLevel, "log-level", "info", "Logging level (debug, info, warn, error)")
flags.StringVar(&cfg.LogFormat, "log-format", "text", "Logging format (text, json). If invalid value is provided, defaults to no logs.")
if err := cmd.MarkFlagRequired("url"); err != nil {
panic(err)
}
if err := cmd.MarkFlagRequired("name"); err != nil {
panic(err)
}
}
func main() {
if err := cmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
func run(ctx context.Context, c Config) error {
// Ensure that the config is valid
if err := c.Validate(); err != nil {
return fmt.Errorf("configuration validation failed: %w", err)
}
logger := c.Logger()
// Create a new scaleset scalesetClient
scalesetClient, err := scaleset.NewClient(c.RegistrationURL, c.ActionsAuth())
if err != nil {
return fmt.Errorf("failed to create scaleset client: %w", err)
}
// Get the runner group ID of the chosen runner group
var runnerGroupID int
switch c.RunnerGroup {
case scaleset.DefaultRunnerGroup:
runnerGroupID = 1
default:
runnerGroup, err := scalesetClient.GetRunnerGroupByName(ctx, c.RunnerGroup)
if err != nil {
return fmt.Errorf("failed to get runner group ID: %w", err)
}
runnerGroupID = runnerGroup.ID
}
// Create the runner scale set
scaleSet, err := scalesetClient.CreateRunnerScaleSet(ctx, &scaleset.RunnerScaleSet{
Name: c.ScaleSetName,
RunnerGroupID: runnerGroupID,
Labels: []scaleset.Label{
{
Name: c.ScaleSetName,
Type: "System",
},
},
RunnerSetting: scaleset.RunnerSetting{
Ephemeral: true,
DisableUpdate: true,
},
})
if err != nil {
return fmt.Errorf("failed to create runner scale set: %w", err)
}
// Set the user agent for the scaleset client now that we have the scale set ID
scalesetClient.SetUserAgent(scaleset.UserAgentInfo{
System: "dockerscaleset",
Version: "0.1.0",
CommitSHA: "unknown",
ScaleSetID: scaleSet.ID,
Subsystem: scaleSet.Name,
})
defer func() {
logger.Info(
"Deleting runner scale set",
slog.Int("scaleSetID", scaleSet.ID),
)
if err := scalesetClient.DeleteRunnerScaleSet(context.WithoutCancel(ctx), scaleSet.ID); err != nil {
slog.Error(
"Failed to delete runner scale set",
slog.Int("scaleSetID", scaleSet.ID),
slog.String("error", err.Error()),
)
}
}()
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("failed to create docker client: %w", err)
}
logger.Info(
"Pulling runner image",
slog.String("image", c.RunnerImage),
)
// Pull the runner image
pull, err := dockerClient.ImagePull(ctx, c.RunnerImage, image.PullOptions{})
if err != nil {
return fmt.Errorf("failed to pull runner image: %w", err)
}
if _, err := io.ReadAll(pull); err != nil {
return fmt.Errorf("failed to read image pull response: %w", err)
}
if err := pull.Close(); err != nil {
return fmt.Errorf("failed to close image pull: %w", err)
}
logger.Info("Initializing listener")
listener, err := listener.New(scalesetClient, listener.Config{
ScaleSetID: scaleSet.ID,
MinRunners: c.MinRunners,
MaxRunners: c.MaxRunners,
Logger: logger.WithGroup("listener"),
})
if err != nil {
return fmt.Errorf("failed to create listener: %w", err)
}
scaler := &Scaler{
logger: logger.WithGroup("scaler"),
runners: runnerState{
idle: make(map[string]string),
busy: make(map[string]string),
},
runnerImage: c.RunnerImage,
minRunners: c.MinRunners,
dockerClient: dockerClient,
scalesetClient: scalesetClient,
scaleSetID: scaleSet.ID,
}
defer scaler.shutdown(context.WithoutCancel(ctx))
logger.Info("Starting listener")
if err := listener.Run(ctx, scaler); !errors.Is(err, context.Canceled) {
return fmt.Errorf("listener run failed: %w", err)
}
return nil
}
var cfg Config
var cmd = &cobra.Command{
Use: "dockerscaleset",
Short: "Example CLI application scaling runners using Docker",
Long: `This is an example CLI application that demonstrates how to scale
runners using Docker.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt)
defer cancel()
if err := cfg.Validate(); err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}
return run(ctx, cfg)
},
}