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:
parent
a55f852161
commit
f40420bfb6
11 changed files with 196 additions and 45 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
55
util/util.go
55
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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue