We'll use GithubEntityType throughout the codebase to determine the type of operation that is about to take place, so this won't belimited to determining only pool type. We'll also use this to dedupe the label scope as well. Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
205 lines
6 KiB
Go
205 lines
6 KiB
Go
package runner
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"html/template"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/cloudbase/garm-provider-common/defaults"
|
|
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
|
|
"github.com/cloudbase/garm/auth"
|
|
"github.com/cloudbase/garm/params"
|
|
)
|
|
|
|
var systemdUnitTemplate = `[Unit]
|
|
Description=GitHub Actions Runner ({{.ServiceName}})
|
|
After=network.target
|
|
|
|
[Service]
|
|
ExecStart=/home/{{.RunAsUser}}/actions-runner/runsvc.sh
|
|
User=runner
|
|
WorkingDirectory=/home/{{.RunAsUser}}/actions-runner
|
|
KillMode=process
|
|
KillSignal=SIGTERM
|
|
TimeoutStopSec=5min
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
`
|
|
|
|
func validateInstanceState(ctx context.Context) (params.Instance, error) {
|
|
if !auth.InstanceHasJITConfig(ctx) {
|
|
return params.Instance{}, fmt.Errorf("instance not configured for JIT: %w", runnerErrors.ErrNotFound)
|
|
}
|
|
|
|
status := auth.InstanceRunnerStatus(ctx)
|
|
if status != params.RunnerPending && status != params.RunnerInstalling {
|
|
return params.Instance{}, runnerErrors.ErrUnauthorized
|
|
}
|
|
|
|
instance, err := auth.InstanceParams(ctx)
|
|
if err != nil {
|
|
return params.Instance{}, runnerErrors.ErrUnauthorized
|
|
}
|
|
return instance, nil
|
|
}
|
|
|
|
func (r *Runner) GetRunnerServiceName(ctx context.Context) (string, error) {
|
|
instance, err := validateInstanceState(ctx)
|
|
if err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
ctx, "failed to get instance params")
|
|
return "", runnerErrors.ErrUnauthorized
|
|
}
|
|
|
|
pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID)
|
|
if err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
ctx, "failed to get pool",
|
|
"pool_id", instance.PoolID)
|
|
return "", errors.Wrap(err, "fetching pool")
|
|
}
|
|
|
|
tpl := "actions.runner.%s.%s"
|
|
var serviceName string
|
|
switch pool.PoolType() {
|
|
case params.GithubEntityTypeEnterprise:
|
|
serviceName = fmt.Sprintf(tpl, pool.EnterpriseName, instance.Name)
|
|
case params.GithubEntityTypeOrganization:
|
|
serviceName = fmt.Sprintf(tpl, pool.OrgName, instance.Name)
|
|
case params.GithubEntityTypeRepository:
|
|
serviceName = fmt.Sprintf(tpl, strings.ReplaceAll(pool.RepoName, "/", "-"), instance.Name)
|
|
}
|
|
return serviceName, nil
|
|
}
|
|
|
|
func (r *Runner) GenerateSystemdUnitFile(ctx context.Context, runAsUser string) ([]byte, error) {
|
|
serviceName, err := r.GetRunnerServiceName(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "fetching runner service name")
|
|
}
|
|
|
|
unitTemplate, err := template.New("").Parse(systemdUnitTemplate)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "parsing template")
|
|
}
|
|
|
|
if runAsUser == "" {
|
|
runAsUser = defaults.DefaultUser
|
|
}
|
|
|
|
data := struct {
|
|
ServiceName string
|
|
RunAsUser string
|
|
}{
|
|
ServiceName: serviceName,
|
|
RunAsUser: runAsUser,
|
|
}
|
|
|
|
var unitFile bytes.Buffer
|
|
if err := unitTemplate.Execute(&unitFile, data); err != nil {
|
|
return nil, errors.Wrap(err, "executing template")
|
|
}
|
|
return unitFile.Bytes(), nil
|
|
}
|
|
|
|
func (r *Runner) GetJITConfigFile(ctx context.Context, file string) ([]byte, error) {
|
|
instance, err := validateInstanceState(ctx)
|
|
if err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
ctx, "failed to get instance params")
|
|
return nil, runnerErrors.ErrUnauthorized
|
|
}
|
|
jitConfig := instance.JitConfiguration
|
|
contents, ok := jitConfig[file]
|
|
if !ok {
|
|
return nil, errors.Wrap(runnerErrors.ErrNotFound, "retrieving file")
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(contents)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "decoding file contents")
|
|
}
|
|
|
|
return decoded, nil
|
|
}
|
|
|
|
func (r *Runner) GetInstanceGithubRegistrationToken(ctx context.Context) (string, error) {
|
|
// Check if this instance already fetched a registration token or if it was configured using
|
|
// the new Just In Time runner feature. If we're still using the old way of configuring a runner,
|
|
// we only allow an instance to fetch one token. If the instance fails to bootstrap after a token
|
|
// is fetched, we reset the token fetched field when re-queueing the instance.
|
|
if auth.InstanceTokenFetched(ctx) || auth.InstanceHasJITConfig(ctx) {
|
|
return "", runnerErrors.ErrUnauthorized
|
|
}
|
|
|
|
status := auth.InstanceRunnerStatus(ctx)
|
|
if status != params.RunnerPending && status != params.RunnerInstalling {
|
|
return "", runnerErrors.ErrUnauthorized
|
|
}
|
|
|
|
instance, err := auth.InstanceParams(ctx)
|
|
if err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
ctx, "failed to get instance params")
|
|
return "", runnerErrors.ErrUnauthorized
|
|
}
|
|
|
|
poolMgr, err := r.getPoolManagerFromInstance(ctx, instance)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "fetching pool manager for instance")
|
|
}
|
|
|
|
token, err := poolMgr.GithubRunnerRegistrationToken()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "fetching runner token")
|
|
}
|
|
|
|
tokenFetched := true
|
|
updateParams := params.UpdateInstanceParams{
|
|
TokenFetched: &tokenFetched,
|
|
}
|
|
|
|
if _, err := r.store.UpdateInstance(r.ctx, instance.ID, updateParams); err != nil {
|
|
return "", errors.Wrap(err, "setting token_fetched for instance")
|
|
}
|
|
|
|
if err := r.store.AddInstanceEvent(ctx, instance.ID, params.FetchTokenEvent, params.EventInfo, "runner registration token was retrieved"); err != nil {
|
|
return "", errors.Wrap(err, "recording event")
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func (r *Runner) GetRootCertificateBundle(ctx context.Context) (params.CertificateBundle, error) {
|
|
instance, err := auth.InstanceParams(ctx)
|
|
if err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
ctx, "failed to get instance params")
|
|
return params.CertificateBundle{}, runnerErrors.ErrUnauthorized
|
|
}
|
|
|
|
poolMgr, err := r.getPoolManagerFromInstance(ctx, instance)
|
|
if err != nil {
|
|
return params.CertificateBundle{}, errors.Wrap(err, "fetching pool manager for instance")
|
|
}
|
|
|
|
bundle, err := poolMgr.RootCABundle()
|
|
if err != nil {
|
|
slog.With(slog.Any("error", err)).ErrorContext(
|
|
ctx, "failed to get root CA bundle",
|
|
"instance", instance.Name,
|
|
"pool_manager", poolMgr.ID())
|
|
// The root CA bundle is invalid. Return an empty bundle to the runner and log the event.
|
|
return params.CertificateBundle{
|
|
RootCertificates: make(map[string][]byte),
|
|
}, nil
|
|
}
|
|
return bundle, nil
|
|
}
|