Add jit config routes

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2023-08-22 11:58:52 +00:00
parent 5214aca228
commit 1268507ce6
8 changed files with 255 additions and 56 deletions

View file

@ -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,71 @@ 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
}
data, err := a.r.GetJITConfigFile(ctx, fileName)
if err != nil {
handleError(w, err)
return
}
// Note the leading dot in the filename
name := fmt.Sprintf("attachment; filename=.%s", fileName)
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()

View file

@ -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("/systemd/service-name/", http.HandlerFunc(han.SystemdServiceNameHandler)).Methods("GET", "OPTIONS")
metadataRouter.Handle("/systemd/service-name/", http.HandlerFunc(han.SystemdServiceNameHandler)).Methods("GET", "OPTIONS")
metadataRouter.Handle("/systemd/runner-service/", http.HandlerFunc(han.SystemdUnitFileHandler)).Methods("GET", "OPTIONS")
metadataRouter.Handle("/systemd/runner-service", 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")

5
go.mod
View file

@ -36,7 +36,10 @@ require (
gorm.io/gorm v1.24.6
)
replace github.com/google/go-github/v54 => github.com/gabriel-samfira/go-github/v54 v54.0.0-20230821112832-bbb536ee5a3a
replace (
github.com/cloudbase/garm-provider-common => /home/ubuntu/garm-provider-common
github.com/google/go-github/v54 => github.com/gabriel-samfira/go-github/v54 v54.0.0-20230821112832-bbb536ee5a3a
)
require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect

2
go.sum
View file

@ -22,8 +22,6 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudbase/garm-provider-common v0.0.0-20230924074517-dd3e26769a05 h1:V9TQBCnwTeKX+gpmlEtZAQ5gNbYrdGelAA3jgnGde1c=
github.com/cloudbase/garm-provider-common v0.0.0-20230924074517-dd3e26769a05/go.mod h1:NgR629o2NYWTffZt3uURmu3orjMDQ2vh6KJ5ytwh7Qw=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View file

@ -1,15 +1,175 @@
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 {
return "", runnerErrors.ErrUnauthorized
}
pool, err := r.store.GetPoolByID(r.ctx, instance.PoolID)
if err != nil {
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, fmt.Errorf("file not found: %w", runnerErrors.ErrNotFound)
}
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 {

View file

@ -800,9 +800,6 @@ func (r *basePoolManager) addInstanceToProvider(instance params.Instance) error
return fmt.Errorf("unknown provider %s for pool %s", pool.ProviderName, pool.ID)
}
// 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.
labels := r.getLabelsForInstance(pool)
jwtValidity := pool.RunnerTimeout()
entity := r.helper.String()
@ -811,6 +808,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,
@ -823,11 +822,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: len(instance.JitConfiguration) > 0,
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
@ -1162,11 +1167,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.

View file

@ -858,52 +858,6 @@ func (r *Runner) AddInstanceStatusMessage(ctx context.Context, param params.Inst
return 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) getPoolManagerFromInstance(ctx context.Context, instance params.Instance) (common.PoolManager, error) {
pool, err := r.store.GetPoolByID(ctx, instance.PoolID)
if err != nil {

3
vendor/modules.txt vendored
View file

@ -14,7 +14,7 @@ github.com/cespare/xxhash/v2
# github.com/chzyer/readline v1.5.1
## explicit; go 1.15
github.com/chzyer/readline
# github.com/cloudbase/garm-provider-common v0.0.0-20230924074517-dd3e26769a05
# github.com/cloudbase/garm-provider-common v0.0.0-20230924074517-dd3e26769a05 => /home/ubuntu/garm-provider-common
## explicit; go 1.20
github.com/cloudbase/garm-provider-common/cloudconfig
github.com/cloudbase/garm-provider-common/defaults
@ -439,4 +439,5 @@ gorm.io/gorm/logger
gorm.io/gorm/migrator
gorm.io/gorm/schema
gorm.io/gorm/utils
# github.com/cloudbase/garm-provider-common => /home/ubuntu/garm-provider-common
# github.com/google/go-github/v54 => github.com/gabriel-samfira/go-github/v54 v54.0.0-20230821112832-bbb536ee5a3a