From f40420bfb64dfe6c47f80094dc40997c8a461fbd Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Wed, 12 Oct 2022 21:45:07 +0000 Subject: [PATCH] Add ability to specify github enpoints for creds The GitHub credentials section now allows setting some API endpoints that point the github client and the runner setup script to the propper URLs. This allows us to use garm with an on-prem github enterprise server. Signed-off-by: Gabriel Adrian Samfira --- cloudconfig/cloudconfig.go | 24 ++++++++++++ cloudconfig/templates.go | 32 +++++++++------ cmd/garm-cli/cmd/credentials.go | 4 +- config/config.go | 69 ++++++++++++++++++++++++++++++--- params/params.go | 13 ++++++- runner/common/pool.go | 4 +- runner/pool/organization.go | 8 ++-- runner/pool/pool.go | 4 +- runner/pool/repository.go | 8 ++-- runner/runner.go | 20 +++++++++- util/util.go | 55 +++++++++++++++++++------- 11 files changed, 196 insertions(+), 45 deletions(-) diff --git a/cloudconfig/cloudconfig.go b/cloudconfig/cloudconfig.go index e90baffc..bf0d3d58 100644 --- a/cloudconfig/cloudconfig.go +++ b/cloudconfig/cloudconfig.go @@ -15,6 +15,7 @@ package cloudconfig import ( + "crypto/x509" "encoding/base64" "fmt" "garm/config" @@ -73,6 +74,29 @@ type CloudInit struct { SystemInfo *SystemInfo `yaml:"system_info,omitempty"` RunCmd []string `yaml:"runcmd,omitempty"` WriteFiles []File `yaml:"write_files,omitempty"` + CACerts CACerts `yaml:"ca-certs,omitempty"` +} + +type CACerts struct { + RemoveDefaults bool `yaml:"remove-defaults"` + Trusted []string `yaml:"trusted"` +} + +func (c *CloudInit) AddCACert(cert []byte) error { + c.mux.Lock() + defer c.mux.Unlock() + + if cert == nil { + return nil + } + + roots := x509.NewCertPool() + if ok := roots.AppendCertsFromPEM(cert); !ok { + return fmt.Errorf("failed to parse CA cert bundle") + } + c.CACerts.Trusted = append(c.CACerts.Trusted, string(cert)) + + return nil } func (c *CloudInit) AddSSHKey(keys ...string) { diff --git a/cloudconfig/templates.go b/cloudconfig/templates.go index 0a4f203c..c68c88ef 100644 --- a/cloudconfig/templates.go +++ b/cloudconfig/templates.go @@ -52,7 +52,16 @@ function fail() { } sendStatus "downloading tools from {{ .DownloadURL }}" -curl -L -o "/home/runner/{{ .FileName }}" "{{ .DownloadURL }}" || fail "failed to download tools" + +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" mkdir -p /home/runner/actions-runner || fail "failed to create actions-runner folder" @@ -84,16 +93,17 @@ success "runner successfully installed" $AGENT_ID ` type InstallRunnerParams struct { - FileName string - DownloadURL string - RunnerUsername string - RunnerGroup string - RepoURL string - GithubToken string - RunnerName string - RunnerLabels string - CallbackURL string - CallbackToken string + FileName string + DownloadURL string + RunnerUsername string + RunnerGroup string + RepoURL string + GithubToken string + RunnerName string + RunnerLabels string + CallbackURL string + CallbackToken string + TempDownloadToken string } func InstallRunnerScript(params InstallRunnerParams) ([]byte, error) { diff --git a/cmd/garm-cli/cmd/credentials.go b/cmd/garm-cli/cmd/credentials.go index 4ecd8f47..a4986b8b 100644 --- a/cmd/garm-cli/cmd/credentials.go +++ b/cmd/garm-cli/cmd/credentials.go @@ -63,10 +63,10 @@ func init() { func formatGithubCredentials(creds []params.GithubCredentials) { t := table.NewWriter() - header := table.Row{"Name", "Description"} + header := table.Row{"Name", "Description", "Base URL", "API URL", "Upload URL"} t.AppendHeader(header) for _, val := range creds { - t.AppendRow(table.Row{val.Name, val.Description}) + t.AppendRow(table.Row{val.Name, val.Description, val.BaseURL, val.APIBaseURL, val.UploadBaseURL}) t.AppendSeparator() } fmt.Println(t.Render()) diff --git a/config/config.go b/config/config.go index dc72937f..8c76441e 100644 --- a/config/config.go +++ b/config/config.go @@ -18,7 +18,6 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io/ioutil" "log" "net" "os" @@ -66,7 +65,13 @@ const ( // of time and no new updates have been made to it's state, it will be removed. DefaultRunnerBootstrapTimeout = 20 + // DefaultGithubURL is the default URL where Github or Github Enterprise can be accessed GithubBaseURL = "https://github.com" + + // defaultBaseURL is the default URL for the github API + defaultBaseURL = "https://api.github.com/" + // uploadBaseURL is the default URL for guthub uploads + uploadBaseURL = "https://uploads.github.com/" ) var ( @@ -190,15 +195,69 @@ func (d *Default) Validate() error { // Github hold configuration options specific to interacting with github. // Currently that is just a OAuth2 personal token. type Github struct { - Name string `toml:"name" json:"name"` - Description string `toml:"description" json:"description"` - OAuth2Token string `toml:"oauth2_token" json:"oauth2-token"` + Name string `toml:"name" json:"name"` + Description string `toml:"description" json:"description"` + OAuth2Token string `toml:"oauth2_token" json:"oauth2-token"` + APIBaseURL string `toml:"api_base_url" json:"api-base-url"` + UploadBaseURL string `toml:"upload_base_url" json:"upload-base-url"` + BaseURL string `toml:"base_url" json:"base-url"` + // CACertBundlePath is the path on disk to a CA certificate bundle that + // can validate the endpoints defined above. Leave empty if not using a + // self signed certificate. + CACertBundlePath string `toml:"ca_cert_bundle" json:"ca-cert-bundle"` +} + +func (g *Github) APIEndpoint() string { + if g.APIBaseURL != "" { + return g.APIBaseURL + } + return defaultBaseURL +} + +func (g *Github) CACertBundle() ([]byte, error) { + if g.CACertBundlePath == "" { + // No CA bundle defined. + return nil, nil + } + if _, err := os.Stat(g.CACertBundlePath); err != nil { + return nil, errors.Wrap(err, "accessing CA bundle") + } + + contents, err := os.ReadFile(g.CACertBundlePath) + if err != nil { + return nil, errors.Wrap(err, "reading CA bundle") + } + + roots := x509.NewCertPool() + if ok := roots.AppendCertsFromPEM(contents); !ok { + return nil, fmt.Errorf("failed to parse CA cert bundle") + } + + return contents, nil +} + +func (g *Github) UploadEndpoint() string { + if g.UploadBaseURL == "" { + if g.APIBaseURL != "" { + return g.APIBaseURL + } + return uploadBaseURL + } + return g.UploadBaseURL +} + +func (g *Github) BaseEndpoint() string { + if g.BaseURL != "" { + return g.BaseURL + } + return GithubBaseURL } func (g *Github) Validate() error { if g.OAuth2Token == "" { return fmt.Errorf("missing github oauth2 token") } + return nil } @@ -372,7 +431,7 @@ func (t *TLSConfig) TLSConfig() (*tls.Config, error) { var roots *x509.CertPool if t.CACert != "" { - caCertPEM, err := ioutil.ReadFile(t.CACert) + caCertPEM, err := os.ReadFile(t.CACert) if err != nil { return nil, err } diff --git a/params/params.go b/params/params.go index 2f1ea43e..8f1f78ba 100644 --- a/params/params.go +++ b/params/params.go @@ -98,6 +98,8 @@ type BootstrapInstance struct { // provider supports it. SSHKeys []string `json:"ssh-keys"` + CACertBundle []byte `json:"ca-cert-bundle"` + OSArch config.OSArch `json:"arch"` Flavor string `json:"flavor"` Image string `json:"image"` @@ -141,6 +143,9 @@ type Internal struct { ControllerID string `json:"controller_id"` InstanceCallbackURL string `json:"instance_callback_url"` JWTSecret string `json:"jwt_secret"` + // GithubCredentialsDetails contains all info about the credentials, except the + // token, which is added above. + GithubCredentialsDetails GithubCredentials `json:"gh_creds_details"` } type Repository struct { @@ -186,8 +191,12 @@ type ControllerInfo struct { } type GithubCredentials struct { - Name string `json:"name,omitempty"` - Description string `json:"description,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + BaseURL string `json:"base_url"` + APIBaseURL string `json:"api_base_url"` + UploadBaseURL string `json:"upload_base_url"` + CABundle []byte `json:"ca_bundle,omitempty"` } type Provider struct { diff --git a/runner/common/pool.go b/runner/common/pool.go index 5da97e95..ca4e31e2 100644 --- a/runner/common/pool.go +++ b/runner/common/pool.go @@ -27,7 +27,9 @@ const ( PoolConsilitationInterval = 5 * time.Second PoolReapTimeoutInterval = 5 * time.Minute - PoolToolUpdateInterval = 3 * time.Hour + // Temporary tools download token is valid for 1 hour by default. + // Set this to less than an hour so as not to run into 401 errors. + PoolToolUpdateInterval = 50 * time.Minute ) type PoolManager interface { diff --git a/runner/pool/organization.go b/runner/pool/organization.go index 546d28b2..4044e6e4 100644 --- a/runner/pool/organization.go +++ b/runner/pool/organization.go @@ -20,7 +20,6 @@ import ( "strings" "sync" - "garm/config" dbCommon "garm/database/common" runnerErrors "garm/errors" "garm/params" @@ -35,7 +34,7 @@ import ( var _ poolHelper = &organization{} func NewOrganizationPoolManager(ctx context.Context, cfg params.Organization, cfgInternal params.Internal, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { - ghc, err := util.GithubClient(ctx, cfgInternal.OAuth2Token) + ghc, err := util.GithubClient(ctx, cfgInternal.OAuth2Token, cfgInternal.GithubCredentialsDetails) if err != nil { return nil, errors.Wrap(err, "getting github client") } @@ -57,6 +56,7 @@ func NewOrganizationPoolManager(ctx context.Context, cfg params.Organization, cf quit: make(chan struct{}), done: make(chan struct{}), helper: helper, + credsDetails: cfgInternal.GithubCredentialsDetails, } return repo, nil } @@ -89,7 +89,7 @@ func (r *organization) UpdateState(param params.UpdatePoolStateParams) error { r.cfg.WebhookSecret = param.WebhookSecret - ghc, err := util.GithubClient(r.ctx, r.GetGithubToken()) + ghc, err := util.GithubClient(r.ctx, r.GetGithubToken(), r.cfgInternal.GithubCredentialsDetails) if err != nil { return errors.Wrap(err, "getting github client") } @@ -138,7 +138,7 @@ func (r *organization) ListPools() ([]params.Pool, error) { } func (r *organization) GithubURL() string { - return fmt.Sprintf("%s/%s", config.GithubBaseURL, r.cfg.Name) + return fmt.Sprintf("%s/%s", r.cfgInternal.GithubCredentialsDetails.BaseURL, r.cfg.Name) } func (r *organization) JwtToken() string { diff --git a/runner/pool/pool.go b/runner/pool/pool.go index 92c9f00c..160da187 100644 --- a/runner/pool/pool.go +++ b/runner/pool/pool.go @@ -58,7 +58,8 @@ type basePool struct { quit chan struct{} done chan struct{} - helper poolHelper + helper poolHelper + credsDetails params.GithubCredentials mux sync.Mutex } @@ -454,6 +455,7 @@ func (r *basePool) addInstanceToProvider(instance params.Instance) error { Image: pool.Image, Labels: labels, PoolID: instance.PoolID, + CACertBundle: r.credsDetails.CABundle, } var instanceIDToDelete string diff --git a/runner/pool/repository.go b/runner/pool/repository.go index 6416efe5..8f0ed6d1 100644 --- a/runner/pool/repository.go +++ b/runner/pool/repository.go @@ -20,7 +20,6 @@ import ( "strings" "sync" - "garm/config" dbCommon "garm/database/common" runnerErrors "garm/errors" "garm/params" @@ -35,7 +34,7 @@ import ( var _ poolHelper = &repository{} func NewRepositoryPoolManager(ctx context.Context, cfg params.Repository, cfgInternal params.Internal, providers map[string]common.Provider, store dbCommon.Store) (common.PoolManager, error) { - ghc, err := util.GithubClient(ctx, cfgInternal.OAuth2Token) + ghc, err := util.GithubClient(ctx, cfgInternal.OAuth2Token, cfgInternal.GithubCredentialsDetails) if err != nil { return nil, errors.Wrap(err, "getting github client") } @@ -57,6 +56,7 @@ func NewRepositoryPoolManager(ctx context.Context, cfg params.Repository, cfgInt quit: make(chan struct{}), done: make(chan struct{}), helper: helper, + credsDetails: cfgInternal.GithubCredentialsDetails, } return repo, nil } @@ -91,7 +91,7 @@ func (r *repository) UpdateState(param params.UpdatePoolStateParams) error { r.cfg.WebhookSecret = param.WebhookSecret - ghc, err := util.GithubClient(r.ctx, r.GetGithubToken()) + ghc, err := util.GithubClient(r.ctx, r.GetGithubToken(), r.cfgInternal.GithubCredentialsDetails) if err != nil { return errors.Wrap(err, "getting github client") } @@ -140,7 +140,7 @@ func (r *repository) ListPools() ([]params.Pool, error) { } func (r *repository) GithubURL() string { - return fmt.Sprintf("%s/%s/%s", config.GithubBaseURL, r.cfg.Owner, r.cfg.Name) + return fmt.Sprintf("%s/%s/%s", r.cfgInternal.GithubCredentialsDetails.BaseURL, r.cfg.Owner, r.cfg.Name) } func (r *repository) JwtToken() string { diff --git a/runner/runner.go b/runner/runner.go index 63664b7c..808edefd 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -190,11 +190,24 @@ func (p *poolManagerCtrl) getInternalConfig(credsName string) (params.Internal, return params.Internal{}, runnerErrors.NewBadRequestError("invalid credential name (%s)", credsName) } + caBundle, err := creds.CACertBundle() + if err != nil { + return params.Internal{}, fmt.Errorf("fetching CA bundle for creds: %w", err) + } + return params.Internal{ OAuth2Token: creds.OAuth2Token, ControllerID: p.controllerID, InstanceCallbackURL: p.config.Default.CallbackURL, JWTSecret: p.config.JWTAuth.Secret, + GithubCredentialsDetails: params.GithubCredentials{ + Name: creds.Name, + Description: creds.Description, + BaseURL: creds.BaseURL, + APIBaseURL: creds.APIBaseURL, + UploadBaseURL: creds.UploadBaseURL, + CABundle: caBundle, + }, }, nil } @@ -219,8 +232,11 @@ func (r *Runner) ListCredentials(ctx context.Context) ([]params.GithubCredential for _, val := range r.config.Github { ret = append(ret, params.GithubCredentials{ - Name: val.Name, - Description: val.Description, + Name: val.Name, + Description: val.Description, + BaseURL: val.BaseEndpoint(), + APIBaseURL: val.APIEndpoint(), + UploadBaseURL: val.UploadEndpoint(), }) } return ret, nil diff --git a/util/util.go b/util/util.go index 993beace..f2ee210b 100644 --- a/util/util.go +++ b/util/util.go @@ -19,10 +19,13 @@ import ( "crypto/aes" "crypto/cipher" "crypto/rand" + "crypto/tls" + "crypto/x509" "encoding/base64" "fmt" "io" "io/ioutil" + "net/http" "os" "path" "regexp" @@ -160,14 +163,33 @@ func OSToOSType(os string) (config.OSType, error) { return osType, nil } -func GithubClient(ctx context.Context, token string) (common.GithubClient, error) { +func GithubClient(ctx context.Context, token string, credsDetails params.GithubCredentials) (common.GithubClient, error) { + var roots *x509.CertPool + if credsDetails.CABundle != nil && len(credsDetails.CABundle) > 0 { + roots = x509.NewCertPool() + ok := roots.AppendCertsFromPEM(credsDetails.CABundle) + if !ok { + return nil, fmt.Errorf("failed to parse CA cert") + } + } + httpTransport := &http.Transport{ + TLSClientConfig: &tls.Config{ + ClientCAs: roots, + }, + } + httpClient := &http.Client{Transport: httpTransport} + ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient) + ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, ) - tc := oauth2.NewClient(ctx, ts) - ghClient := github.NewClient(tc) + // ghClient := github.NewClient(tc) + ghClient, err := github.NewEnterpriseClient(credsDetails.APIBaseURL, credsDetails.UploadBaseURL, tc) + if err != nil { + return nil, errors.Wrap(err, "fetching github client") + } return ghClient.Actions, nil } @@ -176,16 +198,17 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne cloudCfg := cloudconfig.NewDefaultCloudInitConfig() installRunnerParams := cloudconfig.InstallRunnerParams{ - FileName: *tools.Filename, - DownloadURL: *tools.DownloadURL, - GithubToken: bootstrapParams.GithubRunnerAccessToken, - RunnerUsername: config.DefaultUser, - RunnerGroup: config.DefaultUser, - RepoURL: bootstrapParams.RepoURL, - RunnerName: runnerName, - RunnerLabels: strings.Join(bootstrapParams.Labels, ","), - CallbackURL: bootstrapParams.CallbackURL, - CallbackToken: bootstrapParams.InstanceToken, + FileName: *tools.Filename, + DownloadURL: *tools.DownloadURL, + TempDownloadToken: *tools.TempDownloadToken, + GithubToken: bootstrapParams.GithubRunnerAccessToken, + RunnerUsername: config.DefaultUser, + RunnerGroup: config.DefaultUser, + RepoURL: bootstrapParams.RepoURL, + RunnerName: runnerName, + RunnerLabels: strings.Join(bootstrapParams.Labels, ","), + CallbackURL: bootstrapParams.CallbackURL, + CallbackToken: bootstrapParams.InstanceToken, } installScript, err := cloudconfig.InstallRunnerScript(installRunnerParams) @@ -198,6 +221,12 @@ func GetCloudConfig(bootstrapParams params.BootstrapInstance, tools github.Runne cloudCfg.AddRunCmd("/install_runner.sh") cloudCfg.AddRunCmd("rm -f /install_runner.sh") + if bootstrapParams.CACertBundle != nil && len(bootstrapParams.CACertBundle) > 0 { + if err := cloudCfg.AddCACert(bootstrapParams.CACertBundle); err != nil { + return "", errors.Wrap(err, "adding CA cert bundle") + } + } + asStr, err := cloudCfg.Serialize() if err != nil { return "", errors.Wrap(err, "creating cloud config")