Merge pull request #163 from gabriel-samfira/add-jit-config
Add jit config
This commit is contained in:
commit
a48ec0c0a8
18 changed files with 783 additions and 89 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
_, err = r.store.CreateInstance(r.ctx, poolID, createParams)
|
||||
if runner != nil {
|
||||
createParams.AgentID = runner.GetID()
|
||||
}
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue