feat: support routing GitHub API calls to custom proxy API (#1017)

GitHub currently has some limitations w.r.t permissions management on
runner groups as they all require org admin, however at our company
we're using runner groups to serve different internal teams (with
different permissions), thus we needed to deploy a custom proxy API with
our internal authentication to provide who has access to certain APIs
depending on the repository/runner group on a given org/enterprise

This change just allows to optionally send the GitHub API calls to an alternate custom
proxy URL instead of cloud github (github.com) or an enterprise URL with
basic authentication

Co-authored-by: Yusuke Kuoka <ykuoka@gmail.com>
This commit is contained in:
Felipe Galindo Sanchez 2021-12-22 16:24:10 -08:00 committed by GitHub
parent 8a7720da77
commit de1f48111a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 124 additions and 2 deletions

View File

@ -17,6 +17,9 @@ All additional docs are kept in the `docs/` folder, this README is solely for do
| `leaderElectionId` | Set the election ID for the controller group | | | `leaderElectionId` | Set the election ID for the controller group | |
| `githubAPICacheDuration` | Set the cache period for API calls | | | `githubAPICacheDuration` | Set the cache period for API calls | |
| `githubEnterpriseServerURL` | Set the URL for a self-hosted GitHub Enterprise Server | | | `githubEnterpriseServerURL` | Set the URL for a self-hosted GitHub Enterprise Server | |
| `githubURL` | Override GitHub URL to be used for GitHub API calls | |
| `githubUploadURL` | Override GitHub Upload URL to be used for GitHub API calls | |
| `runnerGithubURL` | Override GitHub URL to be used by runners during registration | |
| `logLevel` | Set the log level of the controller container | | | `logLevel` | Set the log level of the controller container | |
| `additionalVolumes` | Set additional volumes to add to the manager container | | | `additionalVolumes` | Set additional volumes to add to the manager container | |
| `additionalVolumeMounts` | Set additional volume mounts to add to the manager container | | | `additionalVolumeMounts` | Set additional volume mounts to add to the manager container | |
@ -27,6 +30,8 @@ All additional docs are kept in the `docs/` folder, this README is solely for do
| `authSecret.github_app_installation_id` | The ID of your GitHub App installation. **This can't be set at the same time as `authSecret.github_token`** | | | `authSecret.github_app_installation_id` | The ID of your GitHub App installation. **This can't be set at the same time as `authSecret.github_token`** | |
| `authSecret.github_app_private_key` | The multiline string of your GitHub App's private key. **This can't be set at the same time as `authSecret.github_token`** | | | `authSecret.github_app_private_key` | The multiline string of your GitHub App's private key. **This can't be set at the same time as `authSecret.github_token`** | |
| `authSecret.github_token` | Your chosen GitHub PAT token. **This can't be set at the same time as the `authSecret.github_app_*`** | | | `authSecret.github_token` | Your chosen GitHub PAT token. **This can't be set at the same time as the `authSecret.github_app_*`** | |
| `authSecret.github_basicauth_username` | Username for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API | |
| `authSecret.github_basicauth_password` | Password for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API | |
| `dockerRegistryMirror` | The default Docker Registry Mirror used by runners. | | | `dockerRegistryMirror` | The default Docker Registry Mirror used by runners. | |
| `image.repository` | The "repository/image" of the controller container | summerwind/actions-runner-controller | | `image.repository` | The "repository/image" of the controller container | summerwind/actions-runner-controller |
| `image.tag` | The tag of the controller container | | | `image.tag` | The tag of the controller container | |

View File

@ -61,6 +61,9 @@ spec:
{{- if .Values.logLevel }} {{- if .Values.logLevel }}
- "--log-level={{ .Values.logLevel }}" - "--log-level={{ .Values.logLevel }}"
{{- end }} {{- end }}
{{- if .Values.runnerGithubURL }}
- "--runner-github-url={{ .Values.runnerGithubURL }}"
{{- end }}
command: command:
- "/manager" - "/manager"
env: env:
@ -68,6 +71,14 @@ spec:
- name: GITHUB_ENTERPRISE_URL - name: GITHUB_ENTERPRISE_URL
value: {{ .Values.githubEnterpriseServerURL }} value: {{ .Values.githubEnterpriseServerURL }}
{{- end }} {{- end }}
{{- if .Values.githubURL }}
- name: GITHUB_URL
value: {{ .Values.githubURL }}
{{- end }}
{{- if .Values.githubUploadURL }}
- name: GITHUB_UPLOAD_URL
value: {{ .Values.githubUploadURL }}
{{- end }}
{{- if .Values.authSecret.enabled }} {{- if .Values.authSecret.enabled }}
- name: GITHUB_TOKEN - name: GITHUB_TOKEN
valueFrom: valueFrom:
@ -93,6 +104,17 @@ spec:
key: github_app_private_key key: github_app_private_key
name: {{ include "actions-runner-controller.secretName" . }} name: {{ include "actions-runner-controller.secretName" . }}
optional: true optional: true
{{- if .Values.authSecret.github_basicauth_username }}
- name: GITHUB_BASICAUTH_USERNAME
value: {{ .Values.authSecret.github_basicauth_username }}
{{- end }}
{{- if .Values.authSecret.github_basicauth_password }}
- name: GITHUB_BASICAUTH_PASSWORD
valueFrom:
secretKeyRef:
key: github_basicauth_password
name: {{ include "actions-runner-controller.secretName" . }}
{{- end }}
{{- end }} {{- end }}
{{- range $key, $val := .Values.env }} {{- range $key, $val := .Values.env }}
- name: {{ $key }} - name: {{ $key }}

View File

@ -45,6 +45,9 @@ spec:
{{- if .Values.scope.singleNamespace }} {{- if .Values.scope.singleNamespace }}
- "--watch-namespace={{ default .Release.Namespace .Values.scope.watchNamespace }}" - "--watch-namespace={{ default .Release.Namespace .Values.scope.watchNamespace }}"
{{- end }} {{- end }}
{{- if .Values.runnerGithubURL }}
- "--runner-github-url={{ .Values.runnerGithubURL }}"
{{- end }}
command: command:
- "/github-webhook-server" - "/github-webhook-server"
env: env:
@ -54,6 +57,18 @@ spec:
key: github_webhook_secret_token key: github_webhook_secret_token
name: {{ include "actions-runner-controller-github-webhook-server.secretName" . }} name: {{ include "actions-runner-controller-github-webhook-server.secretName" . }}
optional: true optional: true
{{- if .Values.githubEnterpriseServerURL }}
- name: GITHUB_ENTERPRISE_URL
value: {{ .Values.githubEnterpriseServerURL }}
{{- end }}
{{- if .Values.githubURL }}
- name: GITHUB_URL
value: {{ .Values.githubURL }}
{{- end }}
{{- if .Values.githubUploadURL }}
- name: GITHUB_UPLOAD_URL
value: {{ .Values.githubUploadURL }}
{{- end }}
{{- if .Values.authSecret.enabled }} {{- if .Values.authSecret.enabled }}
- name: GITHUB_TOKEN - name: GITHUB_TOKEN
valueFrom: valueFrom:
@ -79,6 +94,17 @@ spec:
key: github_app_private_key key: github_app_private_key
name: {{ include "actions-runner-controller.secretName" . }} name: {{ include "actions-runner-controller.secretName" . }}
optional: true optional: true
{{- if .Values.authSecret.github_basicauth_username }}
- name: GITHUB_BASICAUTH_USERNAME
value: {{ .Values.authSecret.github_basicauth_username }}
{{- end }}
{{- if .Values.authSecret.github_basicauth_password }}
- name: GITHUB_BASICAUTH_PASSWORD
valueFrom:
secretKeyRef:
key: github_basicauth_password
name: {{ include "actions-runner-controller.secretName" . }}
{{- end }}
{{- end }} {{- end }}
{{- range $key, $val := .Values.githubWebhookServer.env }} {{- range $key, $val := .Values.githubWebhookServer.env }}
- name: {{ $key }} - name: {{ $key }}

View File

@ -26,4 +26,7 @@ data:
{{- if .Values.authSecret.github_token }} {{- if .Values.authSecret.github_token }}
github_token: {{ .Values.authSecret.github_token | toString | b64enc }} github_token: {{ .Values.authSecret.github_token | toString | b64enc }}
{{- end }} {{- end }}
{{- if .Values.authSecret.github_basicauth_password }}
github_basicauth_password: {{ .Values.authSecret.github_basicauth_password | toString | b64enc }}
{{- end }}
{{- end }} {{- end }}

View File

@ -21,6 +21,11 @@ enableLeaderElection: true
# The URL of your GitHub Enterprise server, if you're using one. # The URL of your GitHub Enterprise server, if you're using one.
#githubEnterpriseServerURL: https://github.example.com #githubEnterpriseServerURL: https://github.example.com
# Override GitHub URLs in case of using proxy APIs
#githubURL: ""
#githubUploadURL: ""
#runnerGithubURL: ""
# Only 1 authentication method can be deployed at a time # Only 1 authentication method can be deployed at a time
# Uncomment the configuration you are applying and fill in the details # Uncomment the configuration you are applying and fill in the details
# #
@ -41,6 +46,9 @@ authSecret:
#github_app_private_key: | #github_app_private_key: |
### GitHub PAT Configuration ### GitHub PAT Configuration
#github_token: "" #github_token: ""
### Basic auth for github API proxy
#github_basicauth_username: ""
#github_basicauth_password: ""
dockerRegistryMirror: "" dockerRegistryMirror: ""
image: image:

View File

@ -103,6 +103,11 @@ func main() {
flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.") 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.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.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
flag.StringVar(&c.URL, "github-url", c.URL, "GitHub URL to be used for GitHub API calls")
flag.StringVar(&c.UploadURL, "github-upload-url", c.UploadURL, "GitHub Upload URL to be used for GitHub API calls")
flag.StringVar(&c.BasicauthUsername, "github-basicauth-username", c.BasicauthUsername, "Username for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API")
flag.StringVar(&c.BasicauthPassword, "github-basicauth-password", c.BasicauthPassword, "Password for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API")
flag.StringVar(&c.RunnerGitHubURL, "runner-github-url", c.RunnerGitHubURL, "GitHub URL to be used by runners during registration")
flag.Parse() flag.Parse()
@ -137,7 +142,7 @@ func main() {
} }
}) })
if len(c.Token) > 0 || (c.AppID > 0 && c.AppInstallationID > 0 && c.AppPrivateKey != "") { if len(c.Token) > 0 || (c.AppID > 0 && c.AppInstallationID > 0 && c.AppPrivateKey != "") || (len(c.BasicauthUsername) > 0 && len(c.BasicauthPassword) > 0) {
ghClient, err = c.NewClient() ghClient, err = c.NewClient()
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err) fmt.Fprintln(os.Stderr, "Error: Client creation failed.", err)

View File

@ -23,6 +23,11 @@ type Config struct {
AppInstallationID int64 `split_words:"true"` AppInstallationID int64 `split_words:"true"`
AppPrivateKey string `split_words:"true"` AppPrivateKey string `split_words:"true"`
Token string Token string
URL string `split_words:"true"`
UploadURL string `split_words:"true"`
BasicauthUsername string `split_words:"true"`
BasicauthPassword string `split_words:"true"`
RunnerGitHubURL string `split_words:"true"`
} }
// Client wraps GitHub client with some additional // Client wraps GitHub client with some additional
@ -34,10 +39,23 @@ type Client struct {
GithubBaseURL string GithubBaseURL string
} }
type BasicAuthTransport struct {
Username string
Password string
}
func (p BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.SetBasicAuth(p.Username, p.Password)
req.Header.Set("User-Agent", "actions-runner-controller")
return http.DefaultTransport.RoundTrip(req)
}
// NewClient creates a Github Client // NewClient creates a Github Client
func (c *Config) NewClient() (*Client, error) { func (c *Config) NewClient() (*Client, error) {
var transport http.RoundTripper var transport http.RoundTripper
if len(c.Token) > 0 { if len(c.BasicauthUsername) > 0 && len(c.BasicauthPassword) > 0 {
transport = BasicAuthTransport{Username: c.BasicauthUsername, Password: c.BasicauthPassword}
} else if len(c.Token) > 0 {
transport = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token})).Transport transport = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.Token})).Transport
} else { } else {
var tr *ghinstallation.Transport var tr *ghinstallation.Transport
@ -63,6 +81,7 @@ func (c *Config) NewClient() (*Client, error) {
} }
transport = tr transport = tr
} }
transport = metrics.Transport{Transport: transport} transport = metrics.Transport{Transport: transport}
httpClient := &http.Client{Transport: transport} httpClient := &http.Client{Transport: transport}
@ -78,6 +97,35 @@ func (c *Config) NewClient() (*Client, error) {
} else { } else {
client = github.NewClient(httpClient) client = github.NewClient(httpClient)
githubBaseURL = "https://github.com/" githubBaseURL = "https://github.com/"
if len(c.URL) > 0 {
baseUrl, err := url.Parse(c.URL)
if err != nil {
return nil, fmt.Errorf("github client creation failed: %v", err)
}
if !strings.HasSuffix(baseUrl.Path, "/") {
baseUrl.Path += "/"
}
client.BaseURL = baseUrl
}
if len(c.UploadURL) > 0 {
uploadUrl, err := url.Parse(c.UploadURL)
if err != nil {
return nil, fmt.Errorf("github client creation failed: %v", err)
}
if !strings.HasSuffix(uploadUrl.Path, "/") {
uploadUrl.Path += "/"
}
client.UploadURL = uploadUrl
}
if len(c.RunnerGitHubURL) > 0 {
githubBaseURL = c.RunnerGitHubURL
if !strings.HasSuffix(githubBaseURL, "/") {
githubBaseURL += "/"
}
}
} }
return &Client{ return &Client{

View File

@ -111,6 +111,11 @@ func main() {
flag.Int64Var(&c.AppID, "github-app-id", c.AppID, "The application ID of GitHub App.") 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.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.StringVar(&c.AppPrivateKey, "github-app-private-key", c.AppPrivateKey, "The path of a private key file to authenticate as a GitHub App")
flag.StringVar(&c.URL, "github-url", c.URL, "GitHub URL to be used for GitHub API calls")
flag.StringVar(&c.UploadURL, "github-upload-url", c.UploadURL, "GitHub Upload URL to be used for GitHub API calls")
flag.StringVar(&c.BasicauthUsername, "github-basicauth-username", c.BasicauthUsername, "Username for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API")
flag.StringVar(&c.BasicauthPassword, "github-basicauth-password", c.BasicauthPassword, "Password for GitHub basic auth to use instead of PAT or GitHub APP in case it's running behind a proxy API")
flag.StringVar(&c.RunnerGitHubURL, "runner-github-url", c.RunnerGitHubURL, "GitHub URL to be used by runners during registration")
flag.DurationVar(&gitHubAPICacheDuration, "github-api-cache-duration", 0, "The duration until the GitHub API cache expires. Setting this to e.g. 10m results in the controller tries its best not to make the same API call within 10m to reduce the chance of being rate-limited. Defaults to mostly the same value as sync-period. If you're tweaking this in order to make autoscaling more responsive, you'll probably want to tweak sync-period, too") flag.DurationVar(&gitHubAPICacheDuration, "github-api-cache-duration", 0, "The duration until the GitHub API cache expires. Setting this to e.g. 10m results in the controller tries its best not to make the same API call within 10m to reduce the chance of being rate-limited. Defaults to mostly the same value as sync-period. If you're tweaking this in order to make autoscaling more responsive, you'll probably want to tweak sync-period, too")
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. . If you're tweaking this in order to make autoscaling more responsive, you'll probably want to tweak github-api-cache-duration, too") 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. . If you're tweaking this in order to make autoscaling more responsive, you'll probably want to tweak github-api-cache-duration, too")
flag.Var(&commonRunnerLabels, "common-runner-labels", "Runner labels in the K1=V1,K2=V2,... format that are inherited all the runners created by the controller. See https://github.com/actions-runner-controller/actions-runner-controller/issues/321 for more information") flag.Var(&commonRunnerLabels, "common-runner-labels", "Runner labels in the K1=V1,K2=V2,... format that are inherited all the runners created by the controller. See https://github.com/actions-runner-controller/actions-runner-controller/issues/321 for more information")