From a078645ab25b52e5c1eaa6bd5dcd66b9295676ff Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Thu, 1 Dec 2022 18:00:22 +0200 Subject: [PATCH] Add token endpoint This change adds a github registration endpoint that instances can use to fetch a github registration token. This change also invalidates disables access to an instance to the token and status updates endpoints once the instance transitions from "pending" or "installing" to any other state. --- apiserver/controllers/instances.go | 14 ++++++ apiserver/routers/routers.go | 2 + auth/context.go | 39 +++++++++++++--- auth/instance_middleware.go | 9 ++++ auth/jwt.go | 3 +- cloudconfig/templates.go | 17 +++++-- config/config.go | 17 ++++++- .../openstack/cloudconfig/install_runner.tpl | 9 +++- .../openstack/garm-external-provider | 29 +++++++++++- database/sql/instances.go | 41 ++++++++++++++--- database/sql/models.go | 27 ++++++------ database/sql/util.go | 31 ++++++------- params/params.go | 9 ++-- params/requests.go | 15 ++++--- runner/pool/pool.go | 44 ++++++++++++------- runner/runner.go | 14 ++++++ testdata/config.toml | 5 +++ util/util.go | 4 +- 18 files changed, 252 insertions(+), 77 deletions(-) diff --git a/apiserver/controllers/instances.go b/apiserver/controllers/instances.go index a8d6fe2f..7ace7fc6 100644 --- a/apiserver/controllers/instances.go +++ b/apiserver/controllers/instances.go @@ -202,3 +202,17 @@ func (a *APIController) InstanceStatusMessageHandler(w http.ResponseWriter, r *h w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) } + +func (a *APIController) InstanceGithubRegistrationTokenHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + token, err := a.r.GetInstanceGithubRegistrationToken(ctx) + if err != nil { + handleError(w, err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(token)) +} diff --git a/apiserver/routers/routers.go b/apiserver/routers/routers.go index 87d7d471..b59119da 100644 --- a/apiserver/routers/routers.go +++ b/apiserver/routers/routers.go @@ -45,6 +45,8 @@ func NewAPIRouter(han *controllers.APIController, logWriter io.Writer, authMiddl callbackRouter := apiSubRouter.PathPrefix("/callbacks").Subrouter() callbackRouter.Handle("/status/", log(logWriter, http.HandlerFunc(han.InstanceStatusMessageHandler))).Methods("POST", "OPTIONS") callbackRouter.Handle("/status", log(logWriter, http.HandlerFunc(han.InstanceStatusMessageHandler))).Methods("POST", "OPTIONS") + callbackRouter.Handle("/token/", log(logWriter, http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler))).Methods("GET", "OPTIONS") + callbackRouter.Handle("/token", log(logWriter, http.HandlerFunc(han.InstanceGithubRegistrationTokenHandler))).Methods("GET", "OPTIONS") callbackRouter.Use(instanceMiddleware.Middleware) // Login authRouter := apiSubRouter.PathPrefix("/auth").Subrouter() diff --git a/auth/context.go b/auth/context.go index 6d86b168..af4cd919 100644 --- a/auth/context.go +++ b/auth/context.go @@ -18,6 +18,7 @@ import ( "context" "garm/params" + "garm/runner/providers/common" ) type contextFlags string @@ -45,11 +46,13 @@ const ( isEnabledFlag contextFlags = "is_enabled" jwtTokenFlag contextFlags = "jwt_token" - instanceIDKey contextFlags = "id" - instanceNameKey contextFlags = "name" - instancePoolIDKey contextFlags = "pool_id" - instancePoolTypeKey contextFlags = "scope" - instanceEntityKey contextFlags = "entity" + instanceIDKey contextFlags = "id" + instanceNameKey contextFlags = "name" + instancePoolIDKey contextFlags = "pool_id" + instancePoolTypeKey contextFlags = "scope" + instanceEntityKey contextFlags = "entity" + instanceRunnerStatus contextFlags = "status" + instanceGithubToken contextFlags = "github_token" ) func SetInstanceID(ctx context.Context, id string) context.Context { @@ -64,6 +67,30 @@ func InstanceID(ctx context.Context) string { return elem.(string) } +func SetInstanceRunnerStatus(ctx context.Context, val common.RunnerStatus) context.Context { + return context.WithValue(ctx, instanceRunnerStatus, val) +} + +func InstanceRunnerStatus(ctx context.Context) common.RunnerStatus { + elem := ctx.Value(instanceRunnerStatus) + if elem == nil { + return common.RunnerPending + } + return elem.(common.RunnerStatus) +} + +func SetInstanceGithubToken(ctx context.Context, val string) context.Context { + return context.WithValue(ctx, instanceGithubToken, val) +} + +func InstanceGithubToken(ctx context.Context) string { + elem := ctx.Value(instanceGithubToken) + if elem == nil { + return "" + } + return elem.(string) +} + func SetInstanceName(ctx context.Context, val string) context.Context { return context.WithValue(ctx, instanceNameKey, val) } @@ -116,6 +143,8 @@ func PopulateInstanceContext(ctx context.Context, instance params.Instance) cont ctx = SetInstanceID(ctx, instance.ID) ctx = SetInstanceName(ctx, instance.Name) ctx = SetInstancePoolID(ctx, instance.PoolID) + ctx = SetInstanceRunnerStatus(ctx, instance.RunnerStatus) + ctx = SetInstanceGithubToken(ctx, string(instance.GithubRegistrationToken)) return ctx } diff --git a/auth/instance_middleware.go b/auth/instance_middleware.go index 3229927b..c8cdc63e 100644 --- a/auth/instance_middleware.go +++ b/auth/instance_middleware.go @@ -26,6 +26,7 @@ import ( runnerErrors "garm/errors" "garm/params" "garm/runner/common" + providerCommon "garm/runner/providers/common" "github.com/golang-jwt/jwt" "github.com/pkg/errors" @@ -145,6 +146,14 @@ func (amw *instanceMiddleware) Middleware(next http.Handler) http.Handler { if InstanceID(ctx) == "" { invalidAuthResponse(w) + return + } + + runnerStatus := InstanceRunnerStatus(ctx) + if runnerStatus != providerCommon.RunnerInstalling && runnerStatus != providerCommon.RunnerPending { + // Instances that have finished installing can no longer authenticate to the API + invalidAuthResponse(w) + return } // ctx = SetJWTClaim(ctx, *claims) diff --git a/auth/jwt.go b/auth/jwt.go index e43571cd..835b5cc7 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -77,8 +77,7 @@ func invalidAuthResponse(w http.ResponseWriter) { w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode( apiParams.APIErrorResponse{ - Error: "Authentication failed", - Details: "Invalid authentication token", + Error: "Authentication failed", }) } diff --git a/cloudconfig/templates.go b/cloudconfig/templates.go index c68c88ef..c3c0f7d5 100644 --- a/cloudconfig/templates.go +++ b/cloudconfig/templates.go @@ -27,7 +27,17 @@ set -ex set -o pipefail CALLBACK_URL="{{ .CallbackURL }}" +TOKEN_URL="{{ .TokenURL }}" BEARER_TOKEN="{{ .CallbackToken }}" +GITHUB_TOKEN="{{ .GithubToken }}" + +if [ -z "$GITHUB_TOKEN" ];then + if [ -z "$TOKEN_URL" ];then + echo "no token is available and TOKEN_URL is not set" + exit 1 + fi + GITHUB_TOKEN=$(curl -s -X GET -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${TOKEN_URL}") +fi function call() { PAYLOAD="$1" @@ -55,13 +65,11 @@ sendStatus "downloading tools from {{ .DownloadURL }}" TEMP_TOKEN="" - - if [ ! -z "{{ .TempDownloadToken }}" ]; then TEMP_TOKEN="Authorization: Bearer {{ .TempDownloadToken }}" fi -curl -L -H "${TEMP_TOKEN}" -o "/home/runner/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools" +curl -L -H "${TEMP_TOKEN}" -o "/home/{{ .RunnerUsername }}/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools" mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder" @@ -74,7 +82,7 @@ cd /home/{{ .RunnerUsername }}/actions-runner sudo ./bin/installdependencies.sh || fail "failed to install dependencies" sendStatus "configuring runner" -sudo -u {{ .RunnerUsername }} -- ./config.sh --unattended --url "{{ .RepoURL }}" --token "{{ .GithubToken }}" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral || fail "failed to configure runner" +sudo -u {{ .RunnerUsername }} -- ./config.sh --unattended --url "{{ .RepoURL }}" --token "$GITHUB_TOKEN" --name "{{ .RunnerName }}" --labels "{{ .RunnerLabels }}" --ephemeral || fail "failed to configure runner" sendStatus "installing runner service" ./svc.sh install {{ .RunnerUsername }} || fail "failed to install service" @@ -99,6 +107,7 @@ type InstallRunnerParams struct { RunnerGroup string RepoURL string GithubToken string + TokenURL string RunnerName string RunnerLabels string CallbackURL string diff --git a/config/config.go b/config/config.go index d02daea5..8f004580 100644 --- a/config/config.go +++ b/config/config.go @@ -20,6 +20,7 @@ import ( "fmt" "log" "net" + "net/url" "os" "path/filepath" "time" @@ -169,8 +170,11 @@ type Default struct { // ConfigDir is the folder where the runner may save any aditional files // or configurations it may need. Things like auto-generated SSH keys that // may be used to access the runner instances. - ConfigDir string `toml:"config_dir,omitempty" json:"config-dir,omitempty"` + ConfigDir string `toml:"config_dir,omitempty" json:"config-dir,omitempty"` + // CallbackURL is the URL where the instances can send back status reports. CallbackURL string `toml:"callback_url" json:"callback-url"` + // TokenURL is the URL where instances can fetch a github runner registration token. + TokenURL string `toml:"token_url" json:"token-url"` // LogFile is the location of the log file. LogFile string `toml:"log_file,omitempty" json:"log-file"` EnableLogStreamer bool `toml:"enable_log_streamer"` @@ -181,6 +185,17 @@ func (d *Default) Validate() error { return fmt.Errorf("missing callback_url") } + _, err := url.Parse(d.CallbackURL) + if err != nil { + return errors.Wrap(err, "validating callback_url") + } + + if d.TokenURL != "" { + if _, err := url.Parse(d.TokenURL); err != nil { + return errors.Wrap(err, "validating token_url") + } + } + if d.ConfigDir == "" { return fmt.Errorf("config_dir cannot be empty") } diff --git a/contrib/providers.d/openstack/cloudconfig/install_runner.tpl b/contrib/providers.d/openstack/cloudconfig/install_runner.tpl index a13f78c3..130344e1 100644 --- a/contrib/providers.d/openstack/cloudconfig/install_runner.tpl +++ b/contrib/providers.d/openstack/cloudconfig/install_runner.tpl @@ -6,11 +6,18 @@ set -o pipefail CALLBACK_URL="GARM_CALLBACK_URL" BEARER_TOKEN="GARM_CALLBACK_TOKEN" DOWNLOAD_URL="GH_DOWNLOAD_URL" +DOWNLOAD_TOKEN="GH_TEMP_DOWNLOAD_TOKEN" FILENAME="GH_FILENAME" TARGET_URL="GH_TARGET_URL" RUNNER_TOKEN="GH_RUNNER_TOKEN" RUNNER_NAME="GH_RUNNER_NAME" RUNNER_LABELS="GH_RUNNER_LABELS" +TEMP_TOKEN="" + +if [ ! -z "$DOWNLOAD_TOKEN" ]; then + TEMP_TOKEN="Authorization: Bearer $DOWNLOAD_TOKEN" +fi + function call() { PAYLOAD="$1" @@ -37,7 +44,7 @@ function fail() { sendStatus "downloading tools from ${DOWNLOAD_URL}" -curl -L -o "/home/runner/${FILENAME}" "${DOWNLOAD_URL}" || fail "failed to download tools" +curl -L -H "${TEMP_TOKEN}" -o "/home/runner/${FILENAME}" "${DOWNLOAD_URL}" || fail "failed to download tools" mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder" diff --git a/contrib/providers.d/openstack/garm-external-provider b/contrib/providers.d/openstack/garm-external-provider index da0876d1..c56c84c8 100755 --- a/contrib/providers.d/openstack/garm-external-provider +++ b/contrib/providers.d/openstack/garm-external-provider @@ -145,6 +145,20 @@ function downloadURL() { echo "${URL}" } +function tempDownloadToken() { + # temp_download_token + [ -z "$1" -o -z "$2" ] && return 1 + GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}" + TOKEN=$(echo "$INPUT" | jq -c -r --arg OS "$1" --arg ARCH "$GH_ARCH" '(.tools[] | select( .os == $OS and .architecture == $ARCH)).temp_download_token') + echo "${TOKEN}" +} + +function runnerTokenURL() { + TOKEN_URL=$(echo "$INPUT" | jq -c -r '."token-url"') + checkValNotNull "${TOKEN_URL}" "token-url" || return $? + echo "${TOKEN_URL}" +} + function downloadFilename() { [ -z "$1" -o -z "$2" ] && return 1 GH_ARCH="${GARM_TO_GH_ARCH_MAP[$2]}" @@ -177,8 +191,19 @@ function repoURL() { echo "${REPO}" } +function getRegistrationTokenFromAPI() { + TOKEN_URL=$(runnerTokenURL) + BEARER_TOKEN=$(callbackToken) + TOKEN=$(curl -s -X GET -H 'Accept: application/json' -H "Authorization: Bearer ${BEARER_TOKEN}" "${TOKEN_URL}") + checkValNotNull "${TOKEN}" "repo_url" || return $? + echo "${TOKEN}" +} + function ghAccessToken() { TOKEN=$(echo "$INPUT" | jq -c -r '.github_runner_access_token') + if [ -z "$TOKEN" ];then + TOKEN=$(getRegistrationTokenFromAPI) + fi checkValNotNull "${TOKEN}" "github_runner_access_token" || return $? echo "${TOKEN}" } @@ -215,6 +240,7 @@ function getCloudConfig() { ARCH=$(requestedArch) DW_URL=$(downloadURL "${OS_TYPE}" "${ARCH}") + DW_TOKEN=$(tempDownloadToken "${OS_TYPE}" "${ARCH}") DW_FILENAME=$(downloadFilename "${OS_TYPE}" "${ARCH}") LABELS=$(labels) @@ -230,6 +256,7 @@ function getCloudConfig() { -e "s|GH_TARGET_URL|$(repoURL)|g" \ -e "s|GH_RUNNER_TOKEN|$(ghAccessToken)|g" \ -e "s|GH_RUNNER_NAME|$(instanceName)|g" \ + -e "s|GH_TEMP_DOWNLOAD_TOKEN|${DW_TOKEN}|g" \ -e "s|GH_RUNNER_LABELS|${LABELS}|g" > ${TMP_SCRIPT} AS_B64=$(base64 -w0 ${TMP_SCRIPT}) @@ -306,7 +333,7 @@ function CreateInstance() { if [ $? -ne 0 ];then CODE=$? # cleanup - rm -f "${CC_FILE}" || true + rm -f "${CC_FILE}" || true openstack server delete "${INSTANCE_NAME}" || true openstack volume delete "${INSTANCE_NAME}" || true set -e diff --git a/database/sql/instances.go b/database/sql/instances.go index 34f4c720..ca75e01f 100644 --- a/database/sql/instances.go +++ b/database/sql/instances.go @@ -16,8 +16,10 @@ package sql import ( "context" + "fmt" runnerErrors "garm/errors" "garm/params" + "garm/util" "github.com/pkg/errors" uuid "github.com/satori/go.uuid" @@ -30,14 +32,22 @@ func (s *sqlDatabase) CreateInstance(ctx context.Context, poolID string, param p if err != nil { return params.Instance{}, errors.Wrap(err, "fetching pool") } + var ghToken []byte + if param.GithubRegistrationToken != nil { + ghToken, err = util.Aes256EncodeString(string(param.GithubRegistrationToken), s.cfg.Passphrase) + if err != nil { + return params.Instance{}, fmt.Errorf("failed to encrypt gh token") + } + } newInstance := Instance{ - Pool: pool, - Name: param.Name, - Status: param.Status, - RunnerStatus: param.RunnerStatus, - OSType: param.OSType, - OSArch: param.OSArch, - CallbackURL: param.CallbackURL, + Pool: pool, + Name: param.Name, + Status: param.Status, + RunnerStatus: param.RunnerStatus, + OSType: param.OSType, + OSArch: param.OSArch, + CallbackURL: param.CallbackURL, + GithubRegistrationToken: ghToken, } q := s.conn.Create(&newInstance) if q.Error != nil { @@ -112,6 +122,15 @@ func (s *sqlDatabase) GetPoolInstanceByName(ctx context.Context, poolID string, if err != nil { return params.Instance{}, errors.Wrap(err, "fetching instance") } + + if instance.GithubRegistrationToken != nil { + token, err := util.Aes256DecodeString(instance.GithubRegistrationToken, s.cfg.Passphrase) + if err != nil { + return params.Instance{}, errors.Wrap(err, "decoing token") + } + instance.GithubRegistrationToken = []byte(token) + } + return s.sqlToParamsInstance(instance), nil } @@ -120,6 +139,14 @@ func (s *sqlDatabase) GetInstanceByName(ctx context.Context, instanceName string if err != nil { return params.Instance{}, errors.Wrap(err, "fetching instance") } + + if instance.GithubRegistrationToken != nil { + token, err := util.Aes256DecodeString(instance.GithubRegistrationToken, s.cfg.Passphrase) + if err != nil { + return params.Instance{}, errors.Wrap(err, "decoing token") + } + instance.GithubRegistrationToken = []byte(token) + } return s.sqlToParamsInstance(instance), nil } diff --git a/database/sql/models.go b/database/sql/models.go index 523c258b..305e17ca 100644 --- a/database/sql/models.go +++ b/database/sql/models.go @@ -127,19 +127,20 @@ type InstanceStatusUpdate struct { type Instance struct { Base - ProviderID *string `gorm:"uniqueIndex"` - Name string `gorm:"uniqueIndex"` - AgentID int64 - OSType config.OSType - OSArch config.OSArch - OSName string - OSVersion string - Addresses []Address `gorm:"foreignKey:InstanceID"` - Status common.InstanceStatus - RunnerStatus common.RunnerStatus - CallbackURL string - ProviderFault []byte `gorm:"type:longblob"` - CreateAttempt int + ProviderID *string `gorm:"uniqueIndex"` + Name string `gorm:"uniqueIndex"` + AgentID int64 + OSType config.OSType + OSArch config.OSArch + OSName string + OSVersion string + Addresses []Address `gorm:"foreignKey:InstanceID"` + Status common.InstanceStatus + RunnerStatus common.RunnerStatus + CallbackURL string + ProviderFault []byte `gorm:"type:longblob"` + CreateAttempt int + GithubRegistrationToken []byte `gorm:"type:longblob"` PoolID uuid.UUID Pool Pool `gorm:"foreignKey:PoolID"` diff --git a/database/sql/util.go b/database/sql/util.go index e8ff2f54..9a573ea7 100644 --- a/database/sql/util.go +++ b/database/sql/util.go @@ -29,21 +29,22 @@ func (s *sqlDatabase) sqlToParamsInstance(instance Instance) params.Instance { id = *instance.ProviderID } ret := params.Instance{ - ID: instance.ID.String(), - ProviderID: id, - AgentID: instance.AgentID, - Name: instance.Name, - OSType: instance.OSType, - OSName: instance.OSName, - OSVersion: instance.OSVersion, - OSArch: instance.OSArch, - Status: instance.Status, - RunnerStatus: instance.RunnerStatus, - PoolID: instance.PoolID.String(), - CallbackURL: instance.CallbackURL, - StatusMessages: []params.StatusMessage{}, - CreateAttempt: instance.CreateAttempt, - UpdatedAt: instance.UpdatedAt, + ID: instance.ID.String(), + ProviderID: id, + AgentID: instance.AgentID, + Name: instance.Name, + OSType: instance.OSType, + OSName: instance.OSName, + OSVersion: instance.OSVersion, + OSArch: instance.OSArch, + Status: instance.Status, + RunnerStatus: instance.RunnerStatus, + PoolID: instance.PoolID.String(), + CallbackURL: instance.CallbackURL, + StatusMessages: []params.StatusMessage{}, + CreateAttempt: instance.CreateAttempt, + UpdatedAt: instance.UpdatedAt, + GithubRegistrationToken: instance.GithubRegistrationToken, } if len(instance.ProviderFault) > 0 { diff --git a/params/params.go b/params/params.go index b0a07450..e1abcdfe 100644 --- a/params/params.go +++ b/params/params.go @@ -73,11 +73,12 @@ type Instance struct { ProviderFault []byte `json:"provider_fault,omitempty"` StatusMessages []StatusMessage `json:"status_messages,omitempty"` + UpdatedAt time.Time `json:"updated_at"` // Do not serialize sensitive info. - CallbackURL string `json:"-"` - CreateAttempt int `json:"-"` - UpdatedAt time.Time `json:"updated_at"` + CallbackURL string `json:"-"` + CreateAttempt int `json:"-"` + GithubRegistrationToken []byte `json:"-"` } type BootstrapInstance struct { @@ -91,6 +92,8 @@ type BootstrapInstance struct { // CallbackUrl is the URL where the instance can send a post, signaling // progress or status. CallbackURL string `json:"callback-url"` + // TokenURL is the URL where instances can fetch github runner registratin tokens. + TokenURL string `json:"token-url"` // InstanceToken is the token that needs to be set by the instance in the headers // in order to send updated back to the garm via CallbackURL. InstanceToken string `json:"instance-token"` diff --git a/params/requests.go b/params/requests.go index e2ded969..ba5e3a35 100644 --- a/params/requests.go +++ b/params/requests.go @@ -107,13 +107,14 @@ type UpdatePoolParams struct { } type CreateInstanceParams struct { - Name string - OSType config.OSType - OSArch config.OSArch - Status common.InstanceStatus - RunnerStatus common.RunnerStatus - CallbackURL string - CreateAttempt int `json:"-"` + Name string + OSType config.OSType + OSArch config.OSArch + Status common.InstanceStatus + RunnerStatus common.RunnerStatus + CallbackURL string + CreateAttempt int `json:"-"` + GithubRegistrationToken []byte `json:"-"` } type CreatePoolParams struct { diff --git a/runner/pool/pool.go b/runner/pool/pool.go index 1eac96c2..d17b7224 100644 --- a/runner/pool/pool.go +++ b/runner/pool/pool.go @@ -345,15 +345,24 @@ func (r *basePoolManager) AddRunner(ctx context.Context, poolID string) error { } name := fmt.Sprintf("garm-%s", uuid.New()) - + tk, err := r.helper.GetGithubRegistrationToken() + if err != nil { + if errors.Is(err, runnerErrors.ErrUnauthorized) { + failureReason := fmt.Sprintf("failed to fetch registration token: %q", err) + r.setPoolRunningState(false, failureReason) + log.Print(failureReason) + } + return errors.Wrap(err, "fetching registration token") + } createParams := params.CreateInstanceParams{ - Name: name, - Status: providerCommon.InstancePendingCreate, - RunnerStatus: providerCommon.RunnerPending, - OSArch: pool.OSArch, - OSType: pool.OSType, - CallbackURL: r.helper.GetCallbackURL(), - CreateAttempt: 1, + Name: name, + Status: providerCommon.InstancePendingCreate, + RunnerStatus: providerCommon.RunnerPending, + OSArch: pool.OSArch, + OSType: pool.OSType, + GithubRegistrationToken: []byte(tk), + CallbackURL: r.helper.GetCallbackURL(), + CreateAttempt: 1, } _, err = r.store.CreateInstance(r.ctx, poolID, createParams) @@ -512,14 +521,17 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error labels = append(labels, r.controllerLabel()) labels = append(labels, r.poolLabel(pool.ID)) - tk, err := r.helper.GetGithubRegistrationToken() - if err != nil { - if errors.Is(err, runnerErrors.ErrUnauthorized) { - failureReason := fmt.Sprintf("failed to fetch registration token: %q", err) - r.setPoolRunningState(false, failureReason) - log.Print(failureReason) + if instance.GithubRegistrationToken == nil { + tk, err := r.helper.GetGithubRegistrationToken() + if err != nil { + if errors.Is(err, runnerErrors.ErrUnauthorized) { + failureReason := fmt.Sprintf("failed to fetch registration token: %q", err) + r.setPoolRunningState(false, failureReason) + log.Print(failureReason) + } + return errors.Wrap(err, "fetching registration token") } - return errors.Wrap(err, "fetching registration token") + instance.GithubRegistrationToken = []byte(tk) } jwtValidity := pool.RunnerTimeout() @@ -538,7 +550,7 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error Name: instance.Name, Tools: r.tools, RepoURL: r.helper.GithubURL(), - GithubRunnerAccessToken: tk, + GithubRunnerAccessToken: string(instance.GithubRegistrationToken), CallbackURL: instance.CallbackURL, InstanceToken: jwtToken, OSArch: pool.OSArch, diff --git a/runner/runner.go b/runner/runner.go index 3c7b00ba..96398a13 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -722,6 +722,20 @@ func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.Inst return nil } +func (r *Runner) GetInstanceGithubRegistrationToken(ctx context.Context) (string, error) { + instanceID := auth.InstanceID(ctx) + if instanceID == "" { + return "", runnerErrors.ErrUnauthorized + } + + status := auth.InstanceRunnerStatus(ctx) + if status != providerCommon.RunnerPending && status != providerCommon.RunnerInstalling { + return "", runnerErrors.ErrUnauthorized + } + token := auth.InstanceGithubToken(ctx) + return token, nil +} + func (r *Runner) ForceDeleteRunner(ctx context.Context, instanceName string) error { if !auth.IsAdmin(ctx) { return runnerErrors.ErrUnauthorized diff --git a/testdata/config.toml b/testdata/config.toml index 7f1c0d42..30867186 100644 --- a/testdata/config.toml +++ b/testdata/config.toml @@ -4,6 +4,11 @@ # the github actions runner. Status messages can be seen by querying the # runner status in garm. callback_url = "https://garm.example.com/api/v1/callbacks/status" + +# This URL is used to retrieve a github runner registration token for a particular +# instance. Once the instance transitions to "installed", this endpoint should +# no longer be accessible. +token_url = "https://garm.example.com/api/v1/callbacks/tokens" # This folder is defined here for future use. Right now, we create a SSH # public/private key-pair. config_dir = "/etc/garm" diff --git a/util/util.go b/util/util.go index 8658f6a5..5e6a85d2 100644 --- a/util/util.go +++ b/util/util.go @@ -24,7 +24,6 @@ import ( "encoding/base64" "fmt" "io" - "io/ioutil" "net/http" "os" "path" @@ -147,7 +146,7 @@ func GetLoggingWriter(cfg *config.Config) (io.Writer, error) { } func ConvertFileToBase64(file string) (string, error) { - bytes, err := ioutil.ReadFile(file) + bytes, err := os.ReadFile(file) if err != nil { return "", errors.Wrap(err, "reading file") } @@ -214,6 +213,7 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne DownloadURL: *tools.DownloadURL, TempDownloadToken: tempToken, GithubToken: bootstrapParams.GithubRunnerAccessToken, + TokenURL: bootstrapParams.TokenURL, RunnerUsername: config.DefaultUser, RunnerGroup: config.DefaultUser, RepoURL: bootstrapParams.RepoURL,