Allow use of client id as an app id (#4057)

This commit is contained in:
Nikola Jokic 2025-05-16 16:21:06 +02:00 committed by GitHub
parent 43f1cd0dac
commit 1dbb88cb9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 59 additions and 47 deletions

View File

@ -17,6 +17,7 @@ githubConfigSecret:
## (Variation B) When using a GitHub App, the syntax is as follows: ## (Variation B) When using a GitHub App, the syntax is as follows:
# githubConfigSecret: # githubConfigSecret:
# # NOTE: IDs MUST be strings, use quotes # # NOTE: IDs MUST be strings, use quotes
# # The github_app_id can be an app_id or the client_id
# github_app_id: "" # github_app_id: ""
# github_app_installation_id: "" # github_app_installation_id: ""
# github_app_private_key: | # github_app_private_key: |

View File

@ -17,8 +17,9 @@ import (
) )
type Config struct { type Config struct {
ConfigureUrl string `json:"configure_url"` ConfigureUrl string `json:"configure_url"`
AppID int64 `json:"app_id"` // AppID can be an ID of the app or the client ID
AppID string `json:"app_id"`
AppInstallationID int64 `json:"app_installation_id"` AppInstallationID int64 `json:"app_installation_id"`
AppPrivateKey string `json:"app_private_key"` AppPrivateKey string `json:"app_private_key"`
Token string `json:"token"` Token string `json:"token"`
@ -62,26 +63,26 @@ func (c *Config) Validate() error {
} }
if len(c.EphemeralRunnerSetNamespace) == 0 || len(c.EphemeralRunnerSetName) == 0 { if len(c.EphemeralRunnerSetNamespace) == 0 || len(c.EphemeralRunnerSetName) == 0 {
return fmt.Errorf("EphemeralRunnerSetNamespace '%s' or EphemeralRunnerSetName '%s' is missing", c.EphemeralRunnerSetNamespace, c.EphemeralRunnerSetName) return fmt.Errorf("EphemeralRunnerSetNamespace %q or EphemeralRunnerSetName %q is missing", c.EphemeralRunnerSetNamespace, c.EphemeralRunnerSetName)
} }
if c.RunnerScaleSetId == 0 { if c.RunnerScaleSetId == 0 {
return fmt.Errorf("RunnerScaleSetId '%d' is missing", c.RunnerScaleSetId) return fmt.Errorf(`RunnerScaleSetId "%d" is missing`, c.RunnerScaleSetId)
} }
if c.MaxRunners < c.MinRunners { if c.MaxRunners < c.MinRunners {
return fmt.Errorf("MinRunners '%d' cannot be greater than MaxRunners '%d'", c.MinRunners, c.MaxRunners) return fmt.Errorf(`MinRunners "%d" cannot be greater than MaxRunners "%d"`, c.MinRunners, c.MaxRunners)
} }
hasToken := len(c.Token) > 0 hasToken := len(c.Token) > 0
hasPrivateKeyConfig := c.AppID > 0 && c.AppPrivateKey != "" hasPrivateKeyConfig := len(c.AppID) > 0 && c.AppPrivateKey != ""
if !hasToken && !hasPrivateKeyConfig { if !hasToken && !hasPrivateKeyConfig {
return fmt.Errorf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey)) return fmt.Errorf(`GitHub auth credential is missing, token length: "%d", appId: %q, installationId: "%d", private key length: "%d"`, len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey))
} }
if hasToken && hasPrivateKeyConfig { if hasToken && hasPrivateKeyConfig {
return fmt.Errorf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey)) return fmt.Errorf(`only one GitHub auth method supported at a time. Have both PAT and App auth: token length: "%d", appId: %q, installationId: "%d", private key length: "%d"`, len(c.Token), c.AppID, c.AppInstallationID, len(c.AppPrivateKey))
} }
return nil return nil

View File

@ -18,7 +18,7 @@ func TestConfigValidationMinMax(t *testing.T) {
Token: "token", Token: "token",
} }
err := config.Validate() err := config.Validate()
assert.ErrorContains(t, err, "MinRunners '5' cannot be greater than MaxRunners '2", "Expected error about MinRunners > MaxRunners") assert.ErrorContains(t, err, `MinRunners "5" cannot be greater than MaxRunners "2"`, "Expected error about MinRunners > MaxRunners")
} }
func TestConfigValidationMissingToken(t *testing.T) { func TestConfigValidationMissingToken(t *testing.T) {
@ -29,27 +29,47 @@ func TestConfigValidationMissingToken(t *testing.T) {
RunnerScaleSetId: 1, RunnerScaleSetId: 1,
} }
err := config.Validate() err := config.Validate()
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey)) expectedError := fmt.Sprintf(`GitHub auth credential is missing, token length: "%d", appId: %q, installationId: "%d", private key length: "%d"`, len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth") assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
} }
func TestConfigValidationAppKey(t *testing.T) { func TestConfigValidationAppKey(t *testing.T) {
config := &Config{ t.Parallel()
AppID: 1,
AppInstallationID: 10, t.Run("app id integer", func(t *testing.T) {
ConfigureUrl: "github.com/some_org/some_repo", t.Parallel()
EphemeralRunnerSetNamespace: "namespace", config := &Config{
EphemeralRunnerSetName: "deployment", AppID: "1",
RunnerScaleSetId: 1, AppInstallationID: 10,
} ConfigureUrl: "github.com/some_org/some_repo",
err := config.Validate() EphemeralRunnerSetNamespace: "namespace",
expectedError := fmt.Sprintf("GitHub auth credential is missing, token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey)) EphemeralRunnerSetName: "deployment",
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth") RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := fmt.Sprintf(`GitHub auth credential is missing, token length: "%d", appId: %q, installationId: "%d", private key length: "%d"`, len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
})
t.Run("app id as client id", func(t *testing.T) {
t.Parallel()
config := &Config{
AppID: "Iv23f8doAlphaNumer1c",
AppInstallationID: 10,
ConfigureUrl: "github.com/some_org/some_repo",
EphemeralRunnerSetNamespace: "namespace",
EphemeralRunnerSetName: "deployment",
RunnerScaleSetId: 1,
}
err := config.Validate()
expectedError := fmt.Sprintf(`GitHub auth credential is missing, token length: "%d", appId: %q, installationId: "%d", private key length: "%d"`, len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
})
} }
func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) { func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
config := &Config{ config := &Config{
AppID: 1, AppID: "1",
AppInstallationID: 10, AppInstallationID: 10,
AppPrivateKey: "asdf", AppPrivateKey: "asdf",
Token: "asdf", Token: "asdf",
@ -59,7 +79,7 @@ func TestConfigValidationOnlyOneTypeOfCredentials(t *testing.T) {
RunnerScaleSetId: 1, RunnerScaleSetId: 1,
} }
err := config.Validate() err := config.Validate()
expectedError := fmt.Sprintf("only one GitHub auth method supported at a time. Have both PAT and App auth: token length: '%d', appId: '%d', installationId: '%d', private key length: '%d", len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey)) expectedError := fmt.Sprintf(`only one GitHub auth method supported at a time. Have both PAT and App auth: token length: "%d", appId: %q, installationId: "%d", private key length: "%d"`, len(config.Token), config.AppID, config.AppInstallationID, len(config.AppPrivateKey))
assert.ErrorContains(t, err, expectedError, "Expected error about missing auth") assert.ErrorContains(t, err, expectedError, "Expected error about missing auth")
} }

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"math" "math"
"net" "net"
"strconv" "strconv"
@ -169,15 +170,6 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
metricsEndpoint = metricsConfig.endpoint metricsEndpoint = metricsConfig.endpoint
} }
var appID int64
if id, ok := secret.Data["github_app_id"]; ok {
var err error
appID, err = strconv.ParseInt(string(id), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to convert github_app_id to int: %v", err)
}
}
var appInstallationID int64 var appInstallationID int64
if id, ok := secret.Data["github_app_installation_id"]; ok { if id, ok := secret.Data["github_app_installation_id"]; ok {
var err error var err error
@ -189,7 +181,7 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
config := listenerconfig.Config{ config := listenerconfig.Config{
ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl, ConfigureUrl: autoscalingListener.Spec.GitHubConfigUrl,
AppID: appID, AppID: string(secret.Data["github_app_id"]),
AppInstallationID: appInstallationID, AppInstallationID: appInstallationID,
AppPrivateKey: string(secret.Data["github_app_private_key"]), AppPrivateKey: string(secret.Data["github_app_private_key"]),
Token: string(secret.Data["github_token"]), Token: string(secret.Data["github_token"]),
@ -207,6 +199,10 @@ func (b *ResourceBuilder) newScaleSetListenerConfig(autoscalingListener *v1alpha
Metrics: autoscalingListener.Spec.Metrics, Metrics: autoscalingListener.Spec.Metrics,
} }
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid listener config: %w", err)
}
var buf bytes.Buffer var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(config); err != nil { if err := json.NewEncoder(&buf).Encode(config); err != nil {
return nil, fmt.Errorf("failed to encode config: %w", err) return nil, fmt.Errorf("failed to encode config: %w", err)
@ -278,9 +274,7 @@ func (b *ResourceBuilder) newScaleSetListenerPod(autoscalingListener *v1alpha1.A
} }
labels := make(map[string]string, len(autoscalingListener.Labels)) labels := make(map[string]string, len(autoscalingListener.Labels))
for key, val := range autoscalingListener.Labels { maps.Copy(labels, autoscalingListener.Labels)
labels[key] = val
}
newRunnerScaleSetListenerPod := &corev1.Pod{ newRunnerScaleSetListenerPod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{

View File

@ -1212,7 +1212,7 @@ func createJWTForGitHubApp(appAuth *GitHubAppAuth) (string, error) {
claims := &jwt.RegisteredClaims{ claims := &jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(issuedAt), IssuedAt: jwt.NewNumericDate(issuedAt),
ExpiresAt: jwt.NewNumericDate(expiresAt), ExpiresAt: jwt.NewNumericDate(expiresAt),
Issuer: strconv.FormatInt(appAuth.AppID, 10), Issuer: appAuth.AppID,
} }
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

View File

@ -57,7 +57,7 @@ func TestClient_Identifier(t *testing.T) {
} }
defaultAppCreds := &actions.ActionsAuth{ defaultAppCreds := &actions.ActionsAuth{
AppCreds: &actions.GitHubAppAuth{ AppCreds: &actions.GitHubAppAuth{
AppID: 123, AppID: "123",
AppInstallationID: 123, AppInstallationID: 123,
AppPrivateKey: "private key", AppPrivateKey: "private key",
}, },
@ -90,7 +90,7 @@ func TestClient_Identifier(t *testing.T) {
old: defaultAppCreds, old: defaultAppCreds,
new: &actions.ActionsAuth{ new: &actions.ActionsAuth{
AppCreds: &actions.GitHubAppAuth{ AppCreds: &actions.GitHubAppAuth{
AppID: 456, AppID: "456",
AppInstallationID: 456, AppInstallationID: 456,
AppPrivateKey: "new private key", AppPrivateKey: "new private key",
}, },

View File

@ -23,7 +23,8 @@ type multiClient struct {
} }
type GitHubAppAuth struct { type GitHubAppAuth struct {
AppID int64 // AppID is the ID or the Client ID of the application
AppID string
AppInstallationID int64 AppInstallationID int64
AppPrivateKey string AppPrivateKey string
} }
@ -124,16 +125,11 @@ func (m *multiClient) GetClientFromSecret(ctx context.Context, githubConfigURL,
return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...) return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...)
} }
parsedAppID, err := strconv.ParseInt(appID, 10, 64)
if err != nil {
return nil, err
}
parsedAppInstallationID, err := strconv.ParseInt(appInstallationID, 10, 64) parsedAppInstallationID, err := strconv.ParseInt(appInstallationID, 10, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
auth.AppCreds = &GitHubAppAuth{AppID: parsedAppID, AppInstallationID: parsedAppInstallationID, AppPrivateKey: appPrivateKey} auth.AppCreds = &GitHubAppAuth{AppID: appID, AppInstallationID: parsedAppInstallationID, AppPrivateKey: appPrivateKey}
return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...) return m.GetClientFor(ctx, githubConfigURL, auth, namespace, options...)
} }

View File

@ -137,7 +137,7 @@ etFcaQuTHEZyRhhJ4BU=
-----END PRIVATE KEY-----` -----END PRIVATE KEY-----`
auth := &GitHubAppAuth{ auth := &GitHubAppAuth{
AppID: 123, AppID: "123",
AppPrivateKey: key, AppPrivateKey: key,
} }
jwt, err := createJWTForGitHubApp(auth) jwt, err := createJWTForGitHubApp(auth)