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 <gsamfira@cloudbasesolutions.com>
This commit is contained in:
Gabriel Adrian Samfira 2022-10-12 21:45:07 +00:00
parent a55f852161
commit f40420bfb6
No known key found for this signature in database
GPG key ID: 7D073DCC2C074CB5
11 changed files with 196 additions and 45 deletions

View file

@ -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) {

View file

@ -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) {

View file

@ -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())

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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")