diff --git a/apiserver/controllers/metadata.go b/apiserver/controllers/metadata.go index c88e68cb5..664d46a63 100644 --- a/apiserver/controllers/metadata.go +++ b/apiserver/controllers/metadata.go @@ -16,8 +16,12 @@ package controllers import ( "encoding/json" + "fmt" "log" "net/http" + + "github.com/cloudbase/garm/apiserver/params" + "github.com/gorilla/mux" ) func (a *APIController) InstanceGithubRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) { @@ -36,6 +40,74 @@ func (a *APIController) InstanceGithubRegistrationTokenHandler(w http.ResponseWr } } +func (a *APIController) JITCredentialsFileHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + fileName, ok := vars["fileName"] + if !ok { + w.WriteHeader(http.StatusNotFound) + if err := json.NewEncoder(w).Encode(params.APIErrorResponse{ + Error: "Not Found", + Details: "Not Found", + }); err != nil { + log.Printf("failed to encode response: %q", err) + } + return + } + + dotFileName := fmt.Sprintf(".%s", fileName) + + data, err := a.r.GetJITConfigFile(ctx, dotFileName) + if err != nil { + log.Printf("getting JIT config file: %s", err) + handleError(w, err) + return + } + + // Note the leading dot in the filename + name := fmt.Sprintf("attachment; filename=%s", dotFileName) + w.Header().Set("Content-Disposition", name) + w.Header().Set("Content-Type", "octet-stream") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(data))) + w.WriteHeader(http.StatusOK) + if _, err := w.Write(data); err != nil { + log.Printf("failed to encode response: %q", err) + } +} + +func (a *APIController) SystemdServiceNameHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + serviceName, err := a.r.GetRunnerServiceName(ctx) + if err != nil { + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte(serviceName)); err != nil { + log.Printf("failed to encode response: %q", err) + } +} + +func (a *APIController) SystemdUnitFileHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + runAsUser := r.URL.Query().Get("runAsUser") + + data, err := a.r.GenerateSystemdUnitFile(ctx, runAsUser) + if err != nil { + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + if _, err := w.Write(data); err != nil { + log.Printf("failed to encode response: %q", err) + } +} + func (a *APIController) RootCertificateBundleHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index 244992fe8..6644a4bae 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -117,6 +117,14 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl // Registration token metadataRouter.Handle("/runner-registration-token/", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS") metadataRouter.Handle("/runner-registration-token", http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler)).Methods("GET", "OPTIONS") + // JIT credential files + metadataRouter.Handle("/credentials/{fileName}/", http.HandlerFunc(han.JITCredentialsFileHandler)).Methods("GET", "OPTIONS") + metadataRouter.Handle("/credentials/{fileName}", http.HandlerFunc(han.JITCredentialsFileHandler)).Methods("GET", "OPTIONS") + // Systemd files + metadataRouter.Handle("/system/service-name/", http.HandlerFunc(han.SystemdServiceNameHandler)).Methods("GET", "OPTIONS") + metadataRouter.Handle("/system/service-name", http.HandlerFunc(han.SystemdServiceNameHandler)).Methods("GET", "OPTIONS") + metadataRouter.Handle("/systemd/unit-file/", http.HandlerFunc(han.SystemdUnitFileHandler)).Methods("GET", "OPTIONS") + metadataRouter.Handle("/systemd/unit-file", http.HandlerFunc(han.SystemdUnitFileHandler)).Methods("GET", "OPTIONS") metadataRouter.Handle("/system/cert-bundle/", http.HandlerFunc(han.RootCertificateBundleHandler)).Methods("GET", "OPTIONS") metadataRouter.Handle("/system/cert-bundle", http.HandlerFunc(han.RootCertificateBundleHandler)).Methods("GET", "OPTIONS") diff --git a/auth/context.go b/auth/context.go index 642735305..bce9f25e8 100644 --- a/auth/context.go +++ b/auth/context.go @@ -39,6 +39,7 @@ const ( instanceEntityKey contextFlags = "entity" instanceRunnerStatus contextFlags = "status" instanceTokenFetched contextFlags = "tokenFetched" + instanceHasJITConfig contextFlags = "hasJITConfig" instanceParams contextFlags = "instanceParams" ) @@ -66,6 +67,18 @@ func InstanceTokenFetched(ctx context.Context) bool { return elem.(bool) } +func SetInstanceHasJITConfig(ctx context.Context, cfg map[string]string) context.Context { + return context.WithValue(ctx, instanceHasJITConfig, len(cfg) > 0) +} + +func InstanceHasJITConfig(ctx context.Context) bool { + elem := ctx.Value(instanceHasJITConfig) + if elem == nil { + return false + } + return elem.(bool) +} + func SetInstanceParams(ctx context.Context, instance params.Instance) context.Context { return context.WithValue(ctx, instanceParams, instance) } @@ -149,6 +162,7 @@ func PopulateInstanceContext(ctx context.Context, instance params.Instance) cont ctx = SetInstancePoolID(ctx, instance.PoolID) ctx = SetInstanceRunnerStatus(ctx, instance.RunnerStatus) ctx = SetInstanceTokenFetched(ctx, instance.TokenFetched) + ctx = SetInstanceHasJITConfig(ctx, instance.JitConfiguration) ctx = SetInstanceParams(ctx, instance) return ctx } diff --git a/database/sql/instances.go b/database/sql/instances.go index a85a22e09..608d4fa6a 100644 --- a/database/sql/instances.go +++ b/database/sql/instances.go @@ -19,6 +19,7 @@ import ( "encoding/json" runnerErrors "github.com/cloudbase/garm-provider-common/errors" + "github.com/cloudbase/garm-provider-common/util" "github.com/cloudbase/garm/params" "github.com/google/uuid" @@ -28,6 +29,25 @@ import ( "gorm.io/gorm/clause" ) +func (s *sqlDatabase) marshalAndSeal(data interface{}) ([]byte, error) { + enc, err := json.Marshal(data) + if err != nil { + return nil, errors.Wrap(err, "marshalling data") + } + return util.Seal(enc, []byte(s.cfg.Passphrase)) +} + +func (s *sqlDatabase) unsealAndUnmarshal(data []byte, target interface{}) error { + decrypted, err := util.Unseal(data, []byte(s.cfg.Passphrase)) + if err != nil { + return errors.Wrap(err, "decrypting data") + } + if err := json.Unmarshal(decrypted, target); err != nil { + return errors.Wrap(err, "unmarshalling data") + } + return nil +} + func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param params.CreateInstanceParams) (params.Instance, error) { pool, err := s.getPoolByID(ctx, poolID) if err != nil { @@ -42,6 +62,14 @@ func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param p } } + var secret []byte + if len(param.JitConfiguration) > 0 { + secret, err = s.marshalAndSeal(param.JitConfiguration) + if err != nil { + return params.Instance{}, errors.Wrap(err, "marshalling jit config") + } + } + newInstance := Instance{ Pool: pool, Name: param.Name, @@ -52,7 +80,9 @@ func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param p CallbackURL: param.CallbackURL, MetadataURL: param.MetadataURL, GitHubRunnerGroup: param.GitHubRunnerGroup, + JitConfiguration: secret, AditionalLabels: labels, + AgentID: param.AgentID, } q := s.conn.Create(&newInstance) if q.Error != nil { @@ -235,6 +265,14 @@ func (s *sqlDatabase) UpdateInstance(ctx context.Context, instanceID string, par instance.TokenFetched = *param.TokenFetched } + if param.JitConfiguration != nil { + secret, err := s.marshalAndSeal(param.JitConfiguration) + if err != nil { + return params.Instance{}, errors.Wrap(err, "marshalling jit config") + } + instance.JitConfiguration = secret + } + instance.ProviderFault = param.ProviderFault q := s.conn.Save(&instance) diff --git a/database/sql/models.go b/database/sql/models.go index ac41f0318..f33fe9c8d 100644 --- a/database/sql/models.go +++ b/database/sql/models.go @@ -155,6 +155,7 @@ type Instance struct { ProviderFault []byte `gorm:"type:longblob"` CreateAttempt int TokenFetched bool + JitConfiguration []byte `gorm:"type:longblob"` GitHubRunnerGroup string AditionalLabels datatypes.JSON diff --git a/database/sql/util.go b/database/sql/util.go index 3f91c5731..ad0a4d8b3 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -41,6 +41,12 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) (params.Instance, e } } + var jitConfig map[string]string + if len(instance.JitConfiguration) > 0 { + if err := s.unsealAndUnmarshal(instance.JitConfiguration, &jitConfig); err != nil { + return params.Instance{}, errors.Wrap(err, "unmarshalling jit configuration") + } + } ret := params.Instance{ ID: instance.ID.String(), ProviderID: id, @@ -59,6 +65,7 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) (params.Instance, e CreateAttempt: instance.CreateAttempt, UpdatedAt: instance.UpdatedAt, TokenFetched: instance.TokenFetched, + JitConfiguration: jitConfig, GitHubRunnerGroup: instance.GitHubRunnerGroup, AditionalLabels: labels, } diff --git a/params/params.go b/params/params.go index 8844a3e5e..eab4a1735 100644 --- a/params/params.go +++ b/params/params.go @@ -156,11 +156,12 @@ type Instance struct { GitHubRunnerGroup string `json:"github-runner-group"` // Do not serialize sensitive info. - CallbackURL string `json:"-"` - MetadataURL string `json:"-"` - CreateAttempt int `json:"-"` - TokenFetched bool `json:"-"` - AditionalLabels []string `json:"-"` + CallbackURL string `json:"-"` + MetadataURL string `json:"-"` + CreateAttempt int `json:"-"` + TokenFetched bool `json:"-"` + AditionalLabels []string `json:"-"` + JitConfiguration map[string]string `json:"-"` } func (i Instance) GetName() string { diff --git a/params/requests.go b/params/requests.go index 8b3336628..74bfeb503 100644 --- a/params/requests.go +++ b/params/requests.go @@ -136,8 +136,10 @@ type CreateInstanceParams struct { // GithubRunnerGroup is the github runner group to which the runner belongs. // The runner group must be created by someone with access to the enterprise. GitHubRunnerGroup string - CreateAttempt int `json:"-"` + CreateAttempt int `json:"-"` + AgentID int64 `json:"-"` AditionalLabels []string + JitConfiguration map[string]string } type CreatePoolParams struct { @@ -198,12 +200,13 @@ type UpdateInstanceParams struct { // for this instance. Addresses []commonParams.Address `json:"addresses,omitempty"` // Status is the status of the instance inside the provider (eg: running, stopped, etc) - Status commonParams.InstanceStatus `json:"status,omitempty"` - RunnerStatus RunnerStatus `json:"runner_status,omitempty"` - ProviderFault []byte `json:"provider_fault,omitempty"` - AgentID int64 `json:"-"` - CreateAttempt int `json:"-"` - TokenFetched *bool `json:"-"` + Status commonParams.InstanceStatus `json:"status,omitempty"` + RunnerStatus RunnerStatus `json:"runner_status,omitempty"` + ProviderFault []byte `json:"provider_fault,omitempty"` + AgentID int64 `json:"-"` + CreateAttempt int `json:"-"` + TokenFetched *bool `json:"-"` + JitConfiguration map[string]string `json:"-"` } type UpdateUserParams struct { diff --git a/runner/common/mocks/GithubClient.go b/runner/common/mocks/GithubClient.go index f351b58e5..b8f450e75 100644 --- a/runner/common/mocks/GithubClient.go +++ b/runner/common/mocks/GithubClient.go @@ -206,6 +206,76 @@ func (_m *GithubClient) DeleteRepoHook(ctx context.Context, owner string, repo s return r0, r1 } +// GenerateOrgJITConfig provides a mock function with given fields: ctx, owner, request +func (_m *GithubClient) GenerateOrgJITConfig(ctx context.Context, owner string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) { + ret := _m.Called(ctx, owner, request) + + var r0 *github.JITRunnerConfig + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error)); ok { + return rf(ctx, owner, request) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *github.GenerateJITConfigRequest) *github.JITRunnerConfig); ok { + r0 = rf(ctx, owner, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.JITRunnerConfig) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *github.GenerateJITConfigRequest) *github.Response); ok { + r1 = rf(ctx, owner, request) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, *github.GenerateJITConfigRequest) error); ok { + r2 = rf(ctx, owner, request) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GenerateRepoJITConfig provides a mock function with given fields: ctx, owner, repo, request +func (_m *GithubClient) GenerateRepoJITConfig(ctx context.Context, owner string, repo string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) { + ret := _m.Called(ctx, owner, repo, request) + + var r0 *github.JITRunnerConfig + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error)); ok { + return rf(ctx, owner, repo, request) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, *github.GenerateJITConfigRequest) *github.JITRunnerConfig); ok { + r0 = rf(ctx, owner, repo, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.JITRunnerConfig) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, *github.GenerateJITConfigRequest) *github.Response); ok { + r1 = rf(ctx, owner, repo, request) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, *github.GenerateJITConfigRequest) error); ok { + r2 = rf(ctx, owner, repo, request) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // GetOrgHook provides a mock function with given fields: ctx, org, id func (_m *GithubClient) GetOrgHook(ctx context.Context, org string, id int64) (*github.Hook, *github.Response, error) { ret := _m.Called(ctx, org, id) @@ -381,6 +451,41 @@ func (_m *GithubClient) ListOrganizationRunnerApplicationDownloads(ctx context.C return r0, r1, r2 } +// ListOrganizationRunnerGroups provides a mock function with given fields: ctx, org, opts +func (_m *GithubClient) ListOrganizationRunnerGroups(ctx context.Context, org string, opts *github.ListOrgRunnerGroupOptions) (*github.RunnerGroups, *github.Response, error) { + ret := _m.Called(ctx, org, opts) + + var r0 *github.RunnerGroups + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) (*github.RunnerGroups, *github.Response, error)); ok { + return rf(ctx, org, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) *github.RunnerGroups); ok { + r0 = rf(ctx, org, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.RunnerGroups) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) *github.Response); ok { + r1 = rf(ctx, org, opts) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, *github.ListOrgRunnerGroupOptions) error); ok { + r2 = rf(ctx, org, opts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // ListOrganizationRunners provides a mock function with given fields: ctx, owner, opts func (_m *GithubClient) ListOrganizationRunners(ctx context.Context, owner string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { ret := _m.Called(ctx, owner, opts) diff --git a/runner/common/mocks/GithubEnterpriseClient.go b/runner/common/mocks/GithubEnterpriseClient.go index 0a0e6bcaf..2fc1442a5 100644 --- a/runner/common/mocks/GithubEnterpriseClient.go +++ b/runner/common/mocks/GithubEnterpriseClient.go @@ -49,6 +49,41 @@ func (_m *GithubEnterpriseClient) CreateRegistrationToken(ctx context.Context, e return r0, r1, r2 } +// GenerateEnterpriseJITConfig provides a mock function with given fields: ctx, enterprise, request +func (_m *GithubEnterpriseClient) GenerateEnterpriseJITConfig(ctx context.Context, enterprise string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) { + ret := _m.Called(ctx, enterprise, request) + + var r0 *github.JITRunnerConfig + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error)); ok { + return rf(ctx, enterprise, request) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *github.GenerateJITConfigRequest) *github.JITRunnerConfig); ok { + r0 = rf(ctx, enterprise, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.JITRunnerConfig) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *github.GenerateJITConfigRequest) *github.Response); ok { + r1 = rf(ctx, enterprise, request) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, *github.GenerateJITConfigRequest) error); ok { + r2 = rf(ctx, enterprise, request) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // ListRunnerApplicationDownloads provides a mock function with given fields: ctx, enterprise func (_m *GithubEnterpriseClient) ListRunnerApplicationDownloads(ctx context.Context, enterprise string) ([]*github.RunnerApplicationDownload, *github.Response, error) { ret := _m.Called(ctx, enterprise) @@ -84,6 +119,41 @@ func (_m *GithubEnterpriseClient) ListRunnerApplicationDownloads(ctx context.Con return r0, r1, r2 } +// ListRunnerGroups provides a mock function with given fields: ctx, enterprise, opts +func (_m *GithubEnterpriseClient) ListRunnerGroups(ctx context.Context, enterprise string, opts *github.ListEnterpriseRunnerGroupOptions) (*github.EnterpriseRunnerGroups, *github.Response, error) { + ret := _m.Called(ctx, enterprise, opts) + + var r0 *github.EnterpriseRunnerGroups + var r1 *github.Response + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListEnterpriseRunnerGroupOptions) (*github.EnterpriseRunnerGroups, *github.Response, error)); ok { + return rf(ctx, enterprise, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *github.ListEnterpriseRunnerGroupOptions) *github.EnterpriseRunnerGroups); ok { + r0 = rf(ctx, enterprise, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*github.EnterpriseRunnerGroups) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *github.ListEnterpriseRunnerGroupOptions) *github.Response); ok { + r1 = rf(ctx, enterprise, opts) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*github.Response) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, *github.ListEnterpriseRunnerGroupOptions) error); ok { + r2 = rf(ctx, enterprise, opts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // ListRunners provides a mock function with given fields: ctx, enterprise, opts func (_m *GithubEnterpriseClient) ListRunners(ctx context.Context, enterprise string, opts *github.ListOptions) (*github.Runners, *github.Response, error) { ret := _m.Called(ctx, enterprise, opts) diff --git a/runner/common/util.go b/runner/common/util.go index 42f1cc081..e9bc1fdc3 100644 --- a/runner/common/util.go +++ b/runner/common/util.go @@ -41,6 +41,8 @@ type GithubClient interface { RemoveRunner(ctx context.Context, owner, repo string, runnerID int64) (*github.Response, error) // CreateRegistrationToken creates a runner registration token for one repository. CreateRegistrationToken(ctx context.Context, owner, repo string) (*github.RegistrationToken, *github.Response, error) + // GenerateRepoJITConfig generates a just-in-time configuration for a repository. + GenerateRepoJITConfig(ctx context.Context, owner, repo string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) // ListOrganizationRunners lists all runners within an organization. ListOrganizationRunners(ctx context.Context, owner string, opts *github.ListOptions) (*github.Runners, *github.Response, error) @@ -51,6 +53,10 @@ type GithubClient interface { RemoveOrganizationRunner(ctx context.Context, owner string, runnerID int64) (*github.Response, error) // CreateOrganizationRegistrationToken creates a runner registration token for an organization. CreateOrganizationRegistrationToken(ctx context.Context, owner string) (*github.RegistrationToken, *github.Response, error) + // GenerateOrgJITConfig generate a just-in-time configuration for an organization. + GenerateOrgJITConfig(ctx context.Context, owner string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) + // ListOrganizationRunnerGroups lists all runner groups within an organization. + ListOrganizationRunnerGroups(ctx context.Context, org string, opts *github.ListOrgRunnerGroupOptions) (*github.RunnerGroups, *github.Response, error) } type GithubEnterpriseClient interface { @@ -63,4 +69,8 @@ type GithubEnterpriseClient interface { // ListRunnerApplicationDownloads returns a list of github runner application downloads for the // various supported operating systems and architectures. ListRunnerApplicationDownloads(ctx context.Context, enterprise string) ([]*github.RunnerApplicationDownload, *github.Response, error) + // GenerateEnterpriseJITConfig generate a just-in-time configuration for an enterprise. + GenerateEnterpriseJITConfig(ctx context.Context, enterprise string, request *github.GenerateJITConfigRequest) (*github.JITRunnerConfig, *github.Response, error) + // ListRunnerGroups lists all self-hosted runner groups configured in an enterprise. + ListRunnerGroups(ctx context.Context, enterprise string, opts *github.ListEnterpriseRunnerGroupOptions) (*github.EnterpriseRunnerGroups, *github.Response, error) } diff --git a/runner/metadata.go b/runner/metadata.go index 9d0a2769c..7fa8f782f 100644 --- a/runner/metadata.go +++ b/runner/metadata.go @@ -1,15 +1,177 @@ package runner import ( + "bytes" "context" + "encoding/base64" + "fmt" + "html/template" "log" + "strings" + "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" "github.com/pkg/errors" ) +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 { + log.Printf("failed to get instance params: %s", err) + return params.Instance{}, runnerErrors.ErrUnauthorized + } + return instance, nil +} + +func (r *Runner) GetRunnerServiceName(ctx context.Context) (string, error) { + instance, err := validateInstanceState(ctx) + if err != nil { + log.Printf("failed to get instance params: %s", err) + return "", runnerErrors.ErrUnauthorized + } + + pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID) + if err != nil { + log.Printf("failed to get pool: %s", err) + return "", errors.Wrap(err, "fetching pool") + } + + tpl := "actions.runner.%s.%s" + var serviceName string + switch pool.PoolType() { + case params.EnterprisePool: + serviceName = fmt.Sprintf(tpl, pool.EnterpriseName, instance.Name) + case params.OrganizationPool: + serviceName = fmt.Sprintf(tpl, pool.OrgName, instance.Name) + case params.RepositoryPool: + serviceName = fmt.Sprintf(tpl, strings.Replace(pool.RepoName, "/", "-", -1), 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 { + log.Printf("failed to get instance params: %s", err) + 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 { + log.Printf("failed to get instance params: %s", err) + 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 { diff --git a/runner/pool/enterprise.go b/runner/pool/enterprise.go index 6ad0d54ba..b7adad768 100644 --- a/runner/pool/enterprise.go +++ b/runner/pool/enterprise.go @@ -2,7 +2,10 @@ package pool import ( "context" + "encoding/base64" + "encoding/json" "fmt" + "log" "net/http" "strings" "sync" @@ -44,6 +47,12 @@ func NewEnterprisePoolManager(ctx context.Context, cfg params.Enterprise, cfgInt store: store, providers: providers, controllerID: cfgInternal.ControllerID, + urls: urls{ + webhookURL: cfgInternal.BaseWebhookURL, + callbackURL: cfgInternal.InstanceCallbackURL, + metadataURL: cfgInternal.InstanceMetadataURL, + controllerWebhookURL: cfgInternal.ControllerWebhookURL, + }, quit: make(chan struct{}), helper: helper, credsDetails: cfgInternal.GithubCredentialsDetails, @@ -65,6 +74,82 @@ type enterprise struct { mux sync.Mutex } +func (r *enterprise) findRunnerGroupByName(ctx context.Context, name string) (*github.EnterpriseRunnerGroup, error) { + // TODO(gabriel-samfira): implement caching + opts := github.ListEnterpriseRunnerGroupOptions{ + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + for { + runnerGroups, ghResp, err := r.ghcEnterpriseCli.ListRunnerGroups(r.ctx, r.cfg.Name, &opts) + if err != nil { + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") + } + return nil, errors.Wrap(err, "fetching runners") + } + for _, runnerGroup := range runnerGroups.RunnerGroups { + if runnerGroup.Name != nil && *runnerGroup.Name == name { + return runnerGroup, nil + } + } + if ghResp.NextPage == 0 { + break + } + opts.Page = ghResp.NextPage + } + + return nil, errors.Wrap(runnerErrors.ErrNotFound, "runner group not found") +} + +func (r *enterprise) GetJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) { + var rg int64 = 1 + if pool.GitHubRunnerGroup != "" { + runnerGroup, err := r.findRunnerGroupByName(ctx, pool.GitHubRunnerGroup) + if err != nil { + return nil, nil, fmt.Errorf("failed to find runner group: %w", err) + } + rg = *runnerGroup.ID + } + + req := github.GenerateJITConfigRequest{ + Name: instance, + RunnerGroupID: rg, + Labels: labels, + // TODO(gabriel-samfira): Should we make this configurable? + WorkFolder: github.String("_work"), + } + jitConfig, resp, err := r.ghcEnterpriseCli.GenerateEnterpriseJITConfig(ctx, r.cfg.Name, &req) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusUnauthorized { + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + + runner = jitConfig.Runner + defer func() { + if err != nil && runner != nil { + _, innerErr := r.ghcEnterpriseCli.RemoveRunner(r.ctx, r.cfg.Name, runner.GetID()) + log.Printf("failed to remove runner: %v", innerErr) + } + }() + + decoded, err := base64.StdEncoding.DecodeString(*jitConfig.EncodedJITConfig) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err) + } + + var ret map[string]string + if err := json.Unmarshal(decoded, &ret); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err) + } + + return ret, jitConfig.Runner, nil +} + func (r *enterprise) GithubCLI() common.GithubClient { return r.ghcli } diff --git a/runner/pool/interfaces.go b/runner/pool/interfaces.go index ef84abdae..f981992b6 100644 --- a/runner/pool/interfaces.go +++ b/runner/pool/interfaces.go @@ -37,6 +37,8 @@ type poolHelper interface { GithubCLI() common.GithubClient + GetJITConfig(ctx context.Context, instanceName string, pool params.Pool, labels []string) (map[string]string, *github.Runner, error) + FetchDbInstances() ([]params.Instance, error) ListPools() ([]params.Pool, error) GithubURL() string diff --git a/runner/pool/organization.go b/runner/pool/organization.go index dc2206e02..539af39a6 100644 --- a/runner/pool/organization.go +++ b/runner/pool/organization.go @@ -16,6 +16,8 @@ package pool import ( "context" + "encoding/base64" + "encoding/json" "fmt" "log" "net/http" @@ -84,6 +86,82 @@ type organization struct { mux sync.Mutex } +func (r *organization) findRunnerGroupByName(ctx context.Context, name string) (*github.RunnerGroup, error) { + // TODO(gabriel-samfira): implement caching + opts := github.ListOrgRunnerGroupOptions{ + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + for { + runnerGroups, ghResp, err := r.ghcli.ListOrganizationRunnerGroups(r.ctx, r.cfg.Name, &opts) + if err != nil { + if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized { + return nil, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners") + } + return nil, errors.Wrap(err, "fetching runners") + } + for _, runnerGroup := range runnerGroups.RunnerGroups { + if runnerGroup.GetName() == name { + return runnerGroup, nil + } + } + if ghResp.NextPage == 0 { + break + } + opts.Page = ghResp.NextPage + } + + return nil, errors.Wrap(runnerErrors.ErrNotFound, "runner group not found") +} + +func (r *organization) GetJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) { + var rg int64 = 1 + if pool.GitHubRunnerGroup != "" { + runnerGroup, err := r.findRunnerGroupByName(ctx, pool.GitHubRunnerGroup) + if err != nil { + return nil, nil, fmt.Errorf("failed to find runner group: %w", err) + } + rg = runnerGroup.GetID() + } + + req := github.GenerateJITConfigRequest{ + Name: instance, + RunnerGroupID: rg, + Labels: labels, + // TODO(gabriel-samfira): Should we make this configurable? + WorkFolder: github.String("_work"), + } + jitConfig, resp, err := r.ghcli.GenerateOrgJITConfig(ctx, r.cfg.Name, &req) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusUnauthorized { + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + + runner = jitConfig.GetRunner() + defer func() { + if err != nil && runner != nil { + _, innerErr := r.ghcli.RemoveOrganizationRunner(r.ctx, r.cfg.Name, runner.GetID()) + log.Printf("failed to remove runner: %v", innerErr) + } + }() + + decoded, err := base64.StdEncoding.DecodeString(jitConfig.GetEncodedJITConfig()) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err) + } + + var ret map[string]string + if err := json.Unmarshal(decoded, &ret); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err) + } + + return ret, runner, nil +} + func (r *organization) GithubCLI() common.GithubClient { return r.ghcli } diff --git a/runner/pool/pool.go b/runner/pool/pool.go index a74c44c49..4ca49c4e7 100644 --- a/runner/pool/pool.go +++ b/runner/pool/pool.go @@ -388,11 +388,18 @@ func (r *basePoolManager) cleanupOrphanedProviderRunners(runners []*github.Runne continue } + pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID) + if err != nil { + return errors.Wrap(err, "fetching instance pool info") + } + switch instance.RunnerStatus { case params.RunnerPending, params.RunnerInstalling: - // runner is still installing. We give it a chance to finish. - r.log("runner %s is still installing, give it a chance to finish", instance.Name) - continue + if time.Since(instance.UpdatedAt).Minutes() < float64(pool.RunnerTimeout()) { + // runner is still installing. We give it a chance to finish. + r.log("runner %s is still installing, give it a chance to finish", instance.Name) + continue + } } if time.Since(instance.UpdatedAt).Minutes() < 5 { @@ -451,20 +458,7 @@ func (r *basePoolManager) reapTimedOutRunners(runners []*github.Runner) error { // * The runner never joined github within the pool timeout // * The runner managed to join github, but the setup process failed later and the runner // never started on the instance. - // - // There are several steps in the user data that sets up the runner: - // * Download and unarchive the runner from github (or used the cached version) - // * Configure runner (connects to github). At this point the runner is seen as offline. - // * Install the service - // * Set SELinux context (if SELinux is enabled) - // * Start the service (if successful, the runner will transition to "online") - // * Get the runner ID - // - // If we fail getting the runner ID after it's started, garm will set the runner status to "failed", - // even though, technically the runner is online and fully functional. This is why we check here for - // both the runner status as reported by GitHub and the runner status as reported by the provider. - // If the runner is "offline" and marked as "failed", it should be safe to reap it. - if runner, ok := runnersByName[instance.Name]; !ok || (runner.GetStatus() == "offline" && instance.RunnerStatus == params.RunnerFailed) { + if runner, ok := runnersByName[instance.Name]; !ok || runner.GetStatus() == "offline" { r.log("reaping timed-out/failed runner %s", instance.Name) if err := r.ForceDeleteRunner(instance); err != nil { r.log("failed to update runner %s status: %s", instance.Name, err) @@ -527,6 +521,18 @@ func (r *basePoolManager) cleanupOrphanedGithubRunners(runners []*github.Runner) // already marked for deletion or is in the process of being deleted. // Let consolidate take care of it. continue + case commonParams.InstancePendingCreate, commonParams.InstanceCreating: + // instance is still being created. We give it a chance to finish. + r.log("instance %s is still being created, give it a chance to finish", dbInstance.Name) + continue + case commonParams.InstanceRunning: + // this check is not strictly needed, but can help avoid unnecessary strain on the provider. + // At worst, we will have a runner that is offline in github for 5 minutes before we reap it. + if time.Since(dbInstance.UpdatedAt).Minutes() < 5 { + // instance was updated recently. We give it a chance to register itself in github. + r.log("instance %s was updated recently, skipping check", dbInstance.Name) + continue + } } pool, err := r.helper.GetPoolByID(dbInstance.PoolID) @@ -680,13 +686,19 @@ func (r *basePoolManager) setInstanceStatus(runnerName string, status commonPara return instance, nil } -func (r *basePoolManager) AddRunner(ctx context.Context, poolID string, aditionalLabels []string) error { +func (r *basePoolManager) AddRunner(ctx context.Context, poolID string, aditionalLabels []string) (err error) { pool, err := r.helper.GetPoolByID(poolID) if err != nil { return errors.Wrap(err, "fetching pool") } name := fmt.Sprintf("%s-%s", pool.GetRunnerPrefix(), util.NewID()) + labels := r.getLabelsForInstance(pool) + // Attempt to create JIT config + jitConfig, runner, err := r.helper.GetJITConfig(ctx, name, pool, labels) + if err != nil { + r.log("failed to get JIT config, falling back to registration token: %s", err) + } createParams := params.CreateInstanceParams{ Name: name, @@ -699,13 +711,35 @@ func (r *basePoolManager) AddRunner(ctx context.Context, poolID string, aditiona CreateAttempt: 1, GitHubRunnerGroup: pool.GitHubRunnerGroup, AditionalLabels: aditionalLabels, + JitConfiguration: jitConfig, + } + + if runner != nil { + createParams.AgentID = runner.GetID() } - _, err = r.store.CreateInstance(r.ctx, poolID, createParams) + instance, err := r.store.CreateInstance(r.ctx, poolID, createParams) if err != nil { return errors.Wrap(err, "creating instance") } + defer func() { + if err != nil { + if instance.ID != "" { + if err := r.ForceDeleteRunner(instance); err != nil { + r.log("failed to cleanup instance: %s", instance.Name) + } + } + + if runner != nil { + _, runnerCleanupErr := r.helper.RemoveGithubRunner(runner.GetID()) + if err != nil { + r.log("failed to remove runner %d: %s", runner.GetID(), runnerCleanupErr) + } + } + } + }() + return nil } @@ -734,6 +768,16 @@ func (r *basePoolManager) setPoolRunningState(isRunning bool, failureReason stri r.mux.Unlock() } +func (r *basePoolManager) getLabelsForInstance(pool params.Pool) []string { + labels := []string{} + for _, tag := range pool.Tags { + labels = append(labels, tag.Name) + } + labels = append(labels, r.controllerLabel()) + labels = append(labels, r.poolLabel(pool.ID)) + return labels +} + func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error { pool, err := r.helper.GetPoolByID(instance.PoolID) if err != nil { @@ -745,13 +789,6 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error return fmt.Errorf("unknown provider %s for pool %s", pool.ProviderName, pool.ID) } - labels := []string{} - for _, tag := range pool.Tags { - labels = append(labels, tag.Name) - } - labels = append(labels, r.controllerLabel()) - labels = append(labels, r.poolLabel(pool.ID)) - jwtValidity := pool.RunnerTimeout() entity := r.helper.String() @@ -760,6 +797,8 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error return errors.Wrap(err, "fetching instance jwt token") } + hasJITConfig := len(instance.JitConfiguration) > 0 + bootstrapArgs := commonParams.BootstrapInstance{ Name: instance.Name, Tools: r.tools, @@ -772,10 +811,17 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error Flavor: pool.Flavor, Image: pool.Image, ExtraSpecs: pool.ExtraSpecs, - Labels: labels, PoolID: instance.PoolID, CACertBundle: r.credsDetails.CABundle, GitHubRunnerGroup: instance.GitHubRunnerGroup, + JitConfigEnabled: hasJITConfig, + } + + if !hasJITConfig { + // We still need the labels here for situations where we don't have a JIT config generated. + // This can happen if GARM is used against an instance of GHES older than version 3.10. + // The labels field should be ignored by providers if JIT config is enabled. + bootstrapArgs.Labels = r.getLabelsForInstance(pool) } var instanceIDToDelete string @@ -1110,11 +1156,12 @@ func (r *basePoolManager) retryFailedInstancesForOnePool(ctx context.Context, po // TODO(gabriel-samfira): Incrementing CreateAttempt should be done within a transaction. // It's fairly safe to do here (for now), as there should be no other code path that updates // an instance in this state. - var tokenFetched bool = false + var tokenFetched bool = len(instance.JitConfiguration) > 0 updateParams := params.UpdateInstanceParams{ CreateAttempt: instance.CreateAttempt + 1, TokenFetched: &tokenFetched, Status: commonParams.InstancePendingCreate, + RunnerStatus: params.RunnerPending, } r.log("queueing previously failed instance %s for retry", instance.Name) // Set instance to pending create and wait for retry. diff --git a/runner/pool/repository.go b/runner/pool/repository.go index b12c87aa3..c74452de8 100644 --- a/runner/pool/repository.go +++ b/runner/pool/repository.go @@ -16,6 +16,8 @@ package pool import ( "context" + "encoding/base64" + "encoding/json" "fmt" "log" "net/http" @@ -86,6 +88,44 @@ type repository struct { mux sync.Mutex } +func (r *repository) GetJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) { + req := github.GenerateJITConfigRequest{ + Name: instance, + // At the repository level we only have the default runner group. + RunnerGroupID: 1, + Labels: labels, + // TODO(gabriel-samfira): Should we make this configurable? + WorkFolder: github.String("_work"), + } + jitConfig, resp, err := r.ghcli.GenerateRepoJITConfig(ctx, r.cfg.Owner, r.cfg.Name, &req) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusUnauthorized { + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + return nil, nil, fmt.Errorf("failed to get JIT config: %w", err) + } + runner = jitConfig.Runner + + defer func() { + if err != nil && runner != nil { + _, innerErr := r.ghcli.RemoveRunner(r.ctx, r.cfg.Owner, r.cfg.Name, runner.GetID()) + log.Printf("failed to remove runner: %v", innerErr) + } + }() + + decoded, err := base64.StdEncoding.DecodeString(jitConfig.GetEncodedJITConfig()) + if err != nil { + return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err) + } + + var ret map[string]string + if err := json.Unmarshal(decoded, &ret); err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err) + } + + return ret, runner, nil +} + func (r *repository) GithubCLI() common.GithubClient { return r.ghcli } diff --git a/runner/runner.go b/runner/runner.go index 76563c6dc..aa4aedbe7 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -858,55 +858,6 @@ func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.Inst return nil } -func (r *Runner) GetInstanceGithubRegistrationToken(ctx context.Context) (string, error) { - instanceName := auth.InstanceName(ctx) - if instanceName == "" { - return "", runnerErrors.ErrUnauthorized - } - - // Check if this instance already fetched a registration token. 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) { - return "", runnerErrors.ErrUnauthorized - } - - status := auth.InstanceRunnerStatus(ctx) - if status != params.RunnerPending && status != params.RunnerInstalling { - return "", runnerErrors.ErrUnauthorized - } - - instance, err := r.store.GetInstanceByName(ctx, instanceName) - if err != nil { - return "", errors.Wrap(err, "fetching instance") - } - - 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) getPoolManagerFromInstance(ctx context.Context, instance params.Instance) (common.PoolManager, error) { pool, err := r.store.GetPoolByID(ctx, instance.PoolID) if err != nil {