From 40c505097863428f5da73f313ca9cd7991c99ace Mon Sep 17 00:00:00 2001 From: Juho Saarinen Date: Wed, 28 Oct 2020 15:15:53 +0200 Subject: [PATCH] Added support for other than public GitHub URL (#146) Refactoring a bit --- .github/workflows/build-runner.yml | 4 +- README.md | 10 +++ controllers/autoscaling_test.go | 5 +- controllers/runner_controller.go | 4 ++ github/github.go | 67 +++++++++++++------ github/github_test.go | 5 +- go.mod | 1 + go.sum | 2 + main.go | 67 ++++--------------- runner/Makefile | 4 +- ...rfile.dindrunner => dindrunner.Dockerfile} | 1 + runner/entrypoint.sh | 13 +++- 12 files changed, 101 insertions(+), 82 deletions(-) rename runner/{Dockerfile.dindrunner => dindrunner.Dockerfile} (95%) diff --git a/.github/workflows/build-runner.yml b/.github/workflows/build-runner.yml index 7c63604a..3f91a9d9 100644 --- a/.github/workflows/build-runner.yml +++ b/.github/workflows/build-runner.yml @@ -50,7 +50,7 @@ jobs: --platform linux/amd64,linux/arm64 \ --tag ${DOCKERHUB_USERNAME}/actions-runner-dind:v${RUNNER_VERSION} \ --tag ${DOCKERHUB_USERNAME}/actions-runner-dind:latest \ - -f Dockerfile.dindrunner . + -f dindrunner.Dockerfile . - name: Login to GitHub Docker Registry run: echo "${DOCKERHUB_PASSWORD}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin @@ -76,4 +76,4 @@ jobs: --platform linux/amd64,linux/arm64 \ --tag ${DOCKERHUB_USERNAME}/actions-runner-dind:v${RUNNER_VERSION} \ --tag ${DOCKERHUB_USERNAME}/actions-runner-dind:latest \ - -f Dockerfile.dindrunner . --push + -f dindrunner.Dockerfile . --push diff --git a/README.md b/README.md index b4587cf5..20d80119 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,16 @@ Install the custom resource and actions-runner-controller itself. This will crea $ kubectl apply -f https://github.com/summerwind/actions-runner-controller/releases/latest/download/actions-runner-controller.yaml ``` +### Github Enterprise support + +If you use either Github Enterprise Cloud or Server (and have recent enought version supporting Actions), you can use **actions-runner-controller** with those, too. Authentication works same way as with public Github (repo and organization level). + +``` +$ kubectl set env deploy controller-manager -c manager GITHUB_ENTERPRISE_URL= +``` + +[Enterprise level](https://docs.github.com/en/enterprise-server@2.22/actions/hosting-your-own-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-an-enterprise) runners are not working yet as there's no API definition for those. + ## Setting up authentication with GitHub API There are two ways for actions-runner-controller to authenticate with the GitHub API: diff --git a/controllers/autoscaling_test.go b/controllers/autoscaling_test.go index aac3ee0d..edaa1a7c 100644 --- a/controllers/autoscaling_test.go +++ b/controllers/autoscaling_test.go @@ -16,7 +16,10 @@ import ( ) func newGithubClient(server *httptest.Server) *github.Client { - client, err := github.NewClientWithAccessToken("token") + c := github.Config{ + Token: "token", + } + client, err := c.NewClient() if err != nil { panic(err) } diff --git a/controllers/runner_controller.go b/controllers/runner_controller.go index bb4cc3dc..7cd00d3d 100644 --- a/controllers/runner_controller.go +++ b/controllers/runner_controller.go @@ -317,6 +317,10 @@ func (r *RunnerReconciler) newPod(runner v1alpha1.Runner) (corev1.Pod, error) { Name: "DOCKERD_IN_RUNNER", Value: fmt.Sprintf("%v", dockerdInRunner), }, + { + Name: "GITHUB_URL", + Value: r.GitHubClient.GithubBaseURL, + }, } env = append(env, runner.Spec.Env...) diff --git a/github/github.go b/github/github.go index bc2d9bc4..440a7fd9 100644 --- a/github/github.go +++ b/github/github.go @@ -9,43 +9,66 @@ import ( "time" "github.com/bradleyfalzon/ghinstallation" + "github.com/go-logr/logr" "github.com/google/go-github/v32/github" "golang.org/x/oauth2" ) +// Config contains configuration for Github client +type Config struct { + Log logr.Logger + EnterpriseURL string `split_words:"true"` + AppID int64 `split_words:"true"` + AppInstallationID int64 `split_words:"true"` + AppPrivateKey string `split_words:"true"` + Token string +} + // Client wraps GitHub client with some additional type Client struct { *github.Client regTokens map[string]*github.RegistrationToken mu sync.Mutex + // GithubBaseURL to Github without API suffix. + GithubBaseURL string } -// NewClient returns a client authenticated as a GitHub App. -func NewClient(appID, installationID int64, privateKeyPath string) (*Client, error) { - tr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, appID, installationID, privateKeyPath) - if err != nil { - return nil, fmt.Errorf("authentication failed: %v", err) +func (c *Config) NewClient() (*Client, error) { + var ( + httpClient *http.Client + client *github.Client + ) + githubBaseURL := "https://github.com/" + if len(c.Token) > 0 { + httpClient = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: c.Token}, + )) + } else { + tr, err := ghinstallation.NewKeyFromFile(http.DefaultTransport, c.AppID, c.AppInstallationID, c.AppPrivateKey) + if err != nil { + c.Log.Error(err, "Authentication failed") + return nil, fmt.Errorf("authentication failed: %v", err) + } + httpClient = &http.Client{Transport: tr} } - gh := github.NewClient(&http.Client{Transport: tr}) + if len(c.EnterpriseURL) > 0 { + var err error + client, err = github.NewEnterpriseClient(c.EnterpriseURL, c.EnterpriseURL, httpClient) + if err != nil { + c.Log.Error(err, "Enterprise client creation failed") + return nil, fmt.Errorf("enterprise client creation failed: %v", err) + } + githubBaseURL = fmt.Sprintf("%s://%s%s", client.BaseURL.Scheme, client.BaseURL.Host, strings.TrimSuffix(client.BaseURL.Path, "api/v3/")) + } else { + client = github.NewClient(httpClient) + } return &Client{ - Client: gh, - regTokens: map[string]*github.RegistrationToken{}, - mu: sync.Mutex{}, - }, nil -} - -// NewClientWithAccessToken returns a client authenticated with personal access token. -func NewClientWithAccessToken(token string) (*Client, error) { - tc := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - )) - - return &Client{ - Client: github.NewClient(tc), - regTokens: map[string]*github.RegistrationToken{}, - mu: sync.Mutex{}, + Client: client, + regTokens: map[string]*github.RegistrationToken{}, + mu: sync.Mutex{}, + GithubBaseURL: githubBaseURL, }, nil } diff --git a/github/github_test.go b/github/github_test.go index 4846cdb0..6776c68f 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -14,7 +14,10 @@ import ( var server *httptest.Server func newTestClient() *Client { - client, err := NewClientWithAccessToken("token") + c := Config{ + Token: "token", + } + client, err := c.NewClient() if err != nil { panic(err) } diff --git a/go.mod b/go.mod index bc75bd5c..9a1de63f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/go-github/v32 v32.1.1-0.20200822031813-d57a3a84ba04 github.com/google/go-querystring v1.0.0 github.com/gorilla/mux v1.8.0 + github.com/kelseyhightower/envconfig v1.4.0 github.com/onsi/ginkgo v1.8.0 github.com/onsi/gomega v1.5.0 github.com/stretchr/testify v1.4.0 // indirect diff --git a/go.sum b/go.sum index 4992be1d..f6b3a764 100644 --- a/go.sum +++ b/go.sum @@ -158,6 +158,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= diff --git a/main.go b/main.go index 21a0bd15..f01a16c9 100644 --- a/main.go +++ b/main.go @@ -20,9 +20,9 @@ import ( "flag" "fmt" "os" - "strconv" "time" + "github.com/kelseyhightower/envconfig" actionsv1alpha1 "github.com/summerwind/actions-runner-controller/api/v1alpha1" "github.com/summerwind/actions-runner-controller/controllers" "github.com/summerwind/actions-runner-controller/github" @@ -62,68 +62,29 @@ func main() { runnerImage string dockerImage string - - ghToken string - ghAppID int64 - ghAppInstallationID int64 - ghAppPrivateKey string ) + var c github.Config + err = envconfig.Process("github", &c) + if err != nil { + fmt.Fprintln(os.Stderr, "Error: Environment variable read failed.") + } + flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") flag.StringVar(&runnerImage, "runner-image", defaultRunnerImage, "The image name of self-hosted runner container.") flag.StringVar(&dockerImage, "docker-image", defaultDockerImage, "The image name of docker sidecar container.") - flag.StringVar(&ghToken, "github-token", "", "The personal access token of GitHub.") - flag.Int64Var(&ghAppID, "github-app-id", 0, "The application ID of GitHub App.") - flag.Int64Var(&ghAppInstallationID, "github-app-installation-id", 0, "The installation ID of GitHub App.") - flag.StringVar(&ghAppPrivateKey, "github-app-private-key", "", "The path of a private key file to authenticate as a GitHub App") + flag.StringVar(&c.Token, "github-token", c.Token, "The personal access token of GitHub.") + flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.") + flag.Int64Var(&c.AppInstallationID, "github-app-installation-id", c.AppInstallationID, "The installation ID of GitHub App.") + flag.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App") flag.DurationVar(&syncPeriod, "sync-period", 10*time.Minute, "Determines the minimum frequency at which K8s resources managed by this controller are reconciled. When you use autoscaling, set to a lower value like 10 minute, because this corresponds to the minimum time to react on demand change") flag.Parse() - if ghToken == "" { - ghToken = os.Getenv("GITHUB_TOKEN") - } - if ghAppID == 0 { - appID, err := strconv.ParseInt(os.Getenv("GITHUB_APP_ID"), 10, 64) - if err == nil { - ghAppID = appID - } - } - if ghAppInstallationID == 0 { - appInstallationID, err := strconv.ParseInt(os.Getenv("GITHUB_APP_INSTALLATION_ID"), 10, 64) - if err == nil { - ghAppInstallationID = appInstallationID - } - } - if ghAppPrivateKey == "" { - ghAppPrivateKey = os.Getenv("GITHUB_APP_PRIVATE_KEY") - } - - if ghAppID != 0 { - if ghAppInstallationID == 0 { - fmt.Fprintln(os.Stderr, "Error: The installation ID must be specified.") - os.Exit(1) - } - - if ghAppPrivateKey == "" { - fmt.Fprintln(os.Stderr, "Error: The path of a private key file must be specified.") - os.Exit(1) - } - - ghClient, err = github.NewClient(ghAppID, ghAppInstallationID, ghAppPrivateKey) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: Failed to create GitHub client: %v\n", err) - os.Exit(1) - } - } else if ghToken != "" { - ghClient, err = github.NewClientWithAccessToken(ghToken) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: Failed to create GitHub client: %v\n", err) - os.Exit(1) - } - } else { - fmt.Fprintln(os.Stderr, "Error: GitHub App credentials or personal access token must be specified.") + ghClient, err = c.NewClient() + if err != nil { + fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err) os.Exit(1) } diff --git a/runner/Makefile b/runner/Makefile index 233c6eb8..10d3e2f9 100644 --- a/runner/Makefile +++ b/runner/Makefile @@ -24,7 +24,7 @@ endif docker-build: docker build --build-arg RUNNER_VERSION=${RUNNER_VERSION} --build-arg DOCKER_VERSION=${DOCKER_VERSION} -t ${NAME}:${TAG} -t ${NAME}:v${RUNNER_VERSION} . - docker build --build-arg RUNNER_VERSION=${RUNNER_VERSION} --build-arg DOCKER_VERSION=${DOCKER_VERSION} -t ${DIND_RUNNER_NAME}:${TAG} -t ${DIND_RUNNER_NAME}:v${RUNNER_VERSION} -f Dockerfile.dindrunner . + docker build --build-arg RUNNER_VERSION=${RUNNER_VERSION} --build-arg DOCKER_VERSION=${DOCKER_VERSION} -t ${DIND_RUNNER_NAME}:${TAG} -t ${DIND_RUNNER_NAME}:v${RUNNER_VERSION} -f dindrunner.Dockerfile . docker-push: @@ -48,5 +48,5 @@ docker-buildx: --build-arg RUNNER_VERSION=${RUNNER_VERSION} \ --build-arg DOCKER_VERSION=${DOCKER_VERSION} \ -t "${DIND_RUNNER_NAME}:latest" \ - -f Dockerfile.dindrunner \ + -f dindrunner.Dockerfile \ . ${PUSH_ARG} diff --git a/runner/Dockerfile.dindrunner b/runner/dindrunner.Dockerfile similarity index 95% rename from runner/Dockerfile.dindrunner rename to runner/dindrunner.Dockerfile index e9712432..e253e56b 100644 --- a/runner/Dockerfile.dindrunner +++ b/runner/dindrunner.Dockerfile @@ -56,6 +56,7 @@ RUN export ARCH=$(echo ${TARGETPLATFORM} | cut -d / -f2) \ echo >&2 "error: failed to download 'docker-${DOCKER_VERSION}' from '${DOCKER_CHANNEL}' for '${ARCH}'"; \ exit 1; \ fi; \ + echo "Downloaded Docker from https://download.docker.com/linux/static/${DOCKER_CHANNEL}/${ARCH}/docker-${DOCKER_VERSION}.tgz"; \ tar --extract \ --file docker.tgz \ --strip-components 1 \ diff --git a/runner/entrypoint.sh b/runner/entrypoint.sh index 3989998e..1945546c 100755 --- a/runner/entrypoint.sh +++ b/runner/entrypoint.sh @@ -1,5 +1,16 @@ #!/bin/bash +if [ -z "${GITHUB_URL}" ]; then + echo "Working with public GitHub" 1>&2 + GITHUB_URL="https://github.com/" +else + length=${#GITHUB_URL} + last_char=${GITHUB_URL:length-1:1} + + [[ $last_char != "/" ]] && GITHUB_URL="$GITHUB_URL/"; : + echo "Github endpoint URL ${GITHUB_URL}" +fi + if [ -z "${RUNNER_NAME}" ]; then echo "RUNNER_NAME must be set" 1>&2 exit 1 @@ -26,7 +37,7 @@ if [ -z "${RUNNER_TOKEN}" ]; then fi cd /runner -./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "https://github.com/${ATTACH}" --token "${RUNNER_TOKEN}" ${LABEL_ARG} +./config.sh --unattended --replace --name "${RUNNER_NAME}" --url "${GITHUB_URL}${ATTACH}" --token "${RUNNER_TOKEN}" ${LABEL_ARG} for f in runsvc.sh RunnerService.js; do diff {bin,patched}/${f} || :