diff --git a/go.mod b/go.mod index e3cbd99..2780563 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 - github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hashicorp/go-version v1.6.0 github.com/joho/godotenv v1.5.1 github.com/onsi/ginkgo/v2 v2.15.0 @@ -27,6 +26,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect diff --git a/pkg/github/actions/api.go b/pkg/github/actions/api.go deleted file mode 100644 index 59d39cf..0000000 --- a/pkg/github/actions/api.go +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package actions - -import ( - "bytes" - "context" - "encoding/json" - "net/http" -) - -func RequestJSON[Req any, Res any](ctx context.Context, client *ActionsClient, method string, path string, body *Req) (*Res, error) { - buffer := bytes.Buffer{} - if body != nil { - if err := json.NewEncoder(&buffer).Encode(body); err != nil { - return nil, err - } - } - - req, err := client.newActionsServiceRequest(ctx, method, path, &buffer) - if err != nil { - return nil, err - } - - response, err := client.Do(req) - if err != nil { - return nil, err - } - defer response.Body.Close() - - if response.StatusCode == http.StatusNoContent { - return nil, nil - } - - if response.StatusCode != http.StatusOK { - return nil, ParseActionsErrorFromResponse(response) - } - - responseModel := new(Res) - if err = json.NewDecoder(response.Body).Decode(&responseModel); err != nil { - return nil, err - } - - return responseModel, nil -} diff --git a/pkg/github/actions/client.go b/pkg/github/actions/client.go index e64303e..1118da2 100644 --- a/pkg/github/actions/client.go +++ b/pkg/github/actions/client.go @@ -6,29 +6,9 @@ package actions import ( "context" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "sync" - "time" "github.com/google/uuid" - "github.com/macstadium/orka-github-actions-integration/pkg/env" - "github.com/macstadium/orka-github-actions-integration/pkg/github" - "github.com/macstadium/orka-github-actions-integration/pkg/github/app" - "github.com/macstadium/orka-github-actions-integration/pkg/github/auth" "github.com/macstadium/orka-github-actions-integration/pkg/github/types" - retryablehttp "github.com/macstadium/orka-github-actions-integration/pkg/http" - "github.com/macstadium/orka-github-actions-integration/pkg/logging" - "github.com/macstadium/orka-github-actions-integration/pkg/utils" - "go.uber.org/zap" -) - -const ( - scaleSetEndpoint = "_apis/runtime/runnerscalesets" - apiVersion = "6.0-preview" ) type ActionsService interface { @@ -42,7 +22,6 @@ type ActionsService interface { CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*types.RunnerScaleSetSession, error) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error - RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*types.RunnerScaleSetSession, error) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*types.AcquirableJobList, error) @@ -50,115 +29,3 @@ type ActionsService interface { GetMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, lastMessageId int64) (*types.RunnerScaleSetMessage, error) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error } - -type ActionsClient struct { - *retryablehttp.Client - - actionsServiceUrl string - adminToken string - adminTokenExpiresAt time.Time - - logger *zap.SugaredLogger - - envData *env.Data - - gitHubConfig *github.GitHubConfig - - // lock for refreshing the adminToken and adminTokenExpiresAt - mu sync.Mutex -} - -func (client *ActionsClient) newActionsServiceRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { - if err := client.updateTokenIfNeeded(ctx); err != nil { - return nil, err - } - - targetURL, err := client.buildURL(path, apiVersion) - if err != nil { - return nil, err - } - - return http.NewRequestWithContext(ctx, method, targetURL.String(), body) -} - -func (client *ActionsClient) updateTokenIfNeeded(ctx context.Context) error { - client.mu.Lock() - defer client.mu.Unlock() - - aboutToExpire := time.Now().Add(60 * time.Second).After(client.adminTokenExpiresAt) - if !aboutToExpire && !client.adminTokenExpiresAt.IsZero() { - return nil - } - - client.logger.Infof("refreshing token for githubConfigUrl %s", client.gitHubConfig.URL) - - accessToken, err := app.FetchAccessToken(ctx, client.envData) - if err != nil { - return fmt.Errorf("failed to get app access token on refresh: %w", err) - } - - authInfo, err := auth.GetAuthorizationInfo(ctx, accessToken, client.envData.GitHubAPIUrl, client.gitHubConfig) - if err != nil { - return fmt.Errorf("failed to get actions service admin authentication info on refresh: %w", err) - } - - client.actionsServiceUrl = authInfo.ActionsServiceUrl - client.adminToken = authInfo.AdminToken - client.adminTokenExpiresAt, err = utils.GetTokenExpirationTime(authInfo.AdminToken) - if err != nil { - return fmt.Errorf("failed to get admin token expire at on refresh: %w", err) - } - - client.Client.Transport = &retryablehttp.ClientTransport{ - ContentType: "application/json", - Token: client.adminToken, - } - - return nil -} - -func (client *ActionsClient) buildURL(path, apiVersion string) (*url.URL, error) { - urlString := fmt.Sprintf("%s/%s", strings.TrimRight(client.actionsServiceUrl, "/"), strings.TrimLeft(path, "/")) - - targetURL, err := url.Parse(urlString) - if err != nil { - return nil, err - } - - query := targetURL.Query() - if query.Get("api-version") == "" { - query.Set("api-version", apiVersion) - } - targetURL.RawQuery = query.Encode() - - return targetURL, nil -} - -func NewActionsClient(ctx context.Context, envData *env.Data, config *github.GitHubConfig) (*ActionsClient, error) { - accessToken, err := app.FetchAccessToken(ctx, envData) - if err != nil { - return nil, fmt.Errorf("failed to get access token from app: %w", err) - } - - authInfo, err := auth.GetAuthorizationInfo(ctx, accessToken, envData.GitHubAPIUrl, config) - if err != nil { - return nil, fmt.Errorf("failed to get actions service auth info: %w", err) - } - - retryableClient, err := retryablehttp.NewClient(&retryablehttp.ClientTransport{ - Token: authInfo.AdminToken, - ContentType: "application/json", - }) - if err != nil { - return nil, err - } - - return &ActionsClient{ - actionsServiceUrl: authInfo.ActionsServiceUrl, - adminToken: authInfo.AdminToken, - Client: retryableClient, - logger: logging.Logger.Named("actions-service"), - envData: envData, - gitHubConfig: config, - }, nil -} diff --git a/pkg/github/actions/errors.go b/pkg/github/actions/errors.go index 293ca8e..8030d0a 100644 --- a/pkg/github/actions/errors.go +++ b/pkg/github/actions/errors.go @@ -5,12 +5,7 @@ package actions import ( - "bytes" - "encoding/json" "fmt" - "io" - "net/http" - "strings" ) type ActionsError struct { @@ -22,41 +17,3 @@ type ActionsError struct { func (e *ActionsError) Error() string { return fmt.Sprintf("%v - had issue communicating with Actions backend: %v", e.StatusCode, e.Message) } - -func ParseActionsErrorFromResponse(response *http.Response) error { - if response.ContentLength == 0 { - message := "Request returned status: " + response.Status - return &ActionsError{ - ExceptionName: "unknown", - Message: message, - StatusCode: response.StatusCode, - } - } - - body, err := io.ReadAll(response.Body) - if err != nil { - return err - } - - body = trimByteOrderMark(body) - contentType, ok := response.Header["Content-Type"] - if ok && len(contentType) > 0 && strings.Contains(contentType[0], "text/plain") { - message := string(body) - statusCode := response.StatusCode - return &ActionsError{ - Message: message, - StatusCode: statusCode, - } - } - - actionsError := &ActionsError{StatusCode: response.StatusCode} - if err := json.Unmarshal(body, &actionsError); err != nil { - return err - } - - return actionsError -} - -func trimByteOrderMark(body []byte) []byte { - return bytes.TrimPrefix(body, []byte("\xef\xbb\xbf")) -} diff --git a/pkg/github/actions/job.go b/pkg/github/actions/job.go deleted file mode 100644 index b2144ec..0000000 --- a/pkg/github/actions/job.go +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package actions - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/macstadium/orka-github-actions-integration/pkg/github/types" - retryablehttp "github.com/macstadium/orka-github-actions-integration/pkg/http" -) - -func (client *ActionsClient) GetAcquirableJobs(ctx context.Context, runnerScaleSetId int) (*types.AcquirableJobList, error) { - path := fmt.Sprintf("/%s/%d/acquirablejobs", scaleSetEndpoint, runnerScaleSetId) - - res, err := RequestJSON[any, types.AcquirableJobList](ctx, client, http.MethodGet, path, nil) - if res == nil { - res = &types.AcquirableJobList{Count: 0, Jobs: []types.AcquirableJob{}} - } - - return res, err -} - -func (client *ActionsClient) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error) { - pathPrefix := fmt.Sprintf("%s/%s/%d", strings.TrimSuffix(client.actionsServiceUrl, "/"), scaleSetEndpoint, runnerScaleSetId) - path := pathPrefix + "/acquirejobs?api-version=6.0-preview" - - body, err := json.Marshal(requestIds) - if err != nil { - return nil, err - } - - response, err := sendMessageQueueRequest(ctx, path, http.MethodPost, &retryablehttp.ClientTransport{ - Token: messageQueueAccessToken, - ContentType: "application/json", - }, bytes.NewBuffer(body)) - if err != nil { - return nil, err - } - - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - return nil, parseMessageQueueResponse(response) - } - - var acquiredJobs *types.Int64List - err = json.NewDecoder(response.Body).Decode(&acquiredJobs) - if err != nil { - return nil, err - } - - return acquiredJobs.Value, nil -} diff --git a/pkg/github/actions/message.go b/pkg/github/actions/message.go deleted file mode 100644 index 1501ab5..0000000 --- a/pkg/github/actions/message.go +++ /dev/null @@ -1,110 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package actions - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - - ghErrors "github.com/macstadium/orka-github-actions-integration/pkg/github/errors" - "github.com/macstadium/orka-github-actions-integration/pkg/github/types" - retryablehttp "github.com/macstadium/orka-github-actions-integration/pkg/http" -) - -func (client *ActionsClient) GetMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, lastMessageId int64) (*types.RunnerScaleSetMessage, error) { - u, err := url.Parse(messageQueueUrl) - if err != nil { - return nil, err - } - - if lastMessageId > 0 { - q := u.Query() - q.Set("lastMessageId", strconv.FormatInt(lastMessageId, 10)) - u.RawQuery = q.Encode() - } - - response, err := sendMessageQueueRequest(ctx, u.String(), http.MethodGet, &retryablehttp.ClientTransport{ - Token: messageQueueAccessToken, - Accept: "application/json; api-version=6.0-preview", - }, nil) - if err != nil { - return nil, err - } - - defer response.Body.Close() - - if response.StatusCode == http.StatusAccepted { - return nil, nil - } - - if response.StatusCode != http.StatusOK { - return nil, parseMessageQueueResponse(response) - } - - var message *types.RunnerScaleSetMessage - err = json.NewDecoder(response.Body).Decode(&message) - if err != nil { - return nil, err - } - - return message, nil -} - -func (client *ActionsClient) DeleteMessage(ctx context.Context, messageQueueUrl, messageQueueAccessToken string, messageId int64) error { - u, err := url.Parse(messageQueueUrl) - if err != nil { - return err - } - - u.Path = fmt.Sprintf("%s/%d", u.Path, messageId) - - response, err := sendMessageQueueRequest(ctx, u.String(), http.MethodDelete, &retryablehttp.ClientTransport{ - Token: messageQueueAccessToken, - ContentType: "application/json", - }, nil) - if err != nil { - return err - } - - defer response.Body.Close() - - if response.StatusCode != http.StatusNoContent { - return parseMessageQueueResponse(response) - } - - return nil -} - -func sendMessageQueueRequest(ctx context.Context, url, httpMethod string, httpTransport *retryablehttp.ClientTransport, body io.Reader) (*http.Response, error) { - httpClient, err := retryablehttp.NewClient(httpTransport) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, httpMethod, url, body) - if err != nil { - return nil, err - } - - return httpClient.Do(req) -} - -func parseMessageQueueResponse(response *http.Response) error { - if response.StatusCode != http.StatusUnauthorized { - return ParseActionsErrorFromResponse(response) - } - - body, err := io.ReadAll(response.Body) - if err != nil { - return err - } - - return &ghErrors.MessageQueueTokenExpiredError{Message: string(trimByteOrderMark(body))} -} diff --git a/pkg/github/actions/runner.go b/pkg/github/actions/runner.go deleted file mode 100644 index 91fca1b..0000000 --- a/pkg/github/actions/runner.go +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package actions - -import ( - "context" - "fmt" - "net/http" - - "github.com/macstadium/orka-github-actions-integration/pkg/github/types" -) - -const ( - runnerEndpoint = "_apis/distributedtask/pools/0/agents" -) - -func (client *ActionsClient) GetRunner(ctx context.Context, runnerName string) (*types.RunnerReference, error) { - path := fmt.Sprintf("/%s?agentName=%s", runnerEndpoint, runnerName) - - runnersList, err := RequestJSON[any, types.RunnerReferenceList](ctx, client, http.MethodGet, path, nil) - if err != nil { - return nil, err - } - - if runnersList.Count == 0 { - return nil, nil - } - - if runnersList.Count > 1 { - return nil, fmt.Errorf("multiple runner found with name %s", runnerName) - } - - return &runnersList.RunnerReferences[0], nil -} - -func (client *ActionsClient) CreateRunner(ctx context.Context, runnerScaleSetID int, runnerName string) (*types.RunnerScaleSetJitRunnerConfig, error) { - path := fmt.Sprintf("/%s/%d/generatejitconfig", scaleSetEndpoint, runnerScaleSetID) - - jitRunnerSetting := &types.RunnerScaleSetJitRunnerSetting{ - Name: runnerName, - } - - return RequestJSON[types.RunnerScaleSetJitRunnerSetting, types.RunnerScaleSetJitRunnerConfig](ctx, client, http.MethodPost, path, jitRunnerSetting) -} - -func (client *ActionsClient) DeleteRunner(ctx context.Context, runnerID int) error { - path := fmt.Sprintf("/%s/%d", runnerEndpoint, runnerID) - - _, err := RequestJSON[any, any](ctx, client, http.MethodDelete, path, nil) - - return err -} diff --git a/pkg/github/actions/runnerscaleset.go b/pkg/github/actions/runnerscaleset.go deleted file mode 100644 index f50c163..0000000 --- a/pkg/github/actions/runnerscaleset.go +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package actions - -import ( - "context" - "fmt" - "net/http" - - "github.com/macstadium/orka-github-actions-integration/pkg/github/types" -) - -func (client *ActionsClient) GetRunnerScaleSet(ctx context.Context, runnerGroupId int, runnerName string) (*types.RunnerScaleSet, error) { - path := fmt.Sprintf("/%s?runnerGroupId=%d&name=%s", scaleSetEndpoint, runnerGroupId, runnerName) - - runnerScaleSetList, err := RequestJSON[any, types.RunnersListResponse](ctx, client, http.MethodGet, path, nil) - if err != nil { - return nil, err - } - - if runnerScaleSetList.Count == 0 { - return nil, nil - } - - if runnerScaleSetList.Count > 1 { - return nil, fmt.Errorf("multiple runner scale sets found with name %s", runnerName) - } - - return &runnerScaleSetList.Runners[0], nil -} - -func (client *ActionsClient) CreateRunnerScaleSet(ctx context.Context, runner *types.RunnerScaleSet) (*types.RunnerScaleSet, error) { - return RequestJSON[types.RunnerScaleSet, types.RunnerScaleSet](ctx, client, http.MethodPost, scaleSetEndpoint, runner) -} - -func (client *ActionsClient) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetId int) error { - path := fmt.Sprintf("/%s/%d", scaleSetEndpoint, runnerScaleSetId) - - _, err := RequestJSON[any, any](ctx, client, http.MethodDelete, path, nil) - - return err -} diff --git a/pkg/github/actions/session.go b/pkg/github/actions/session.go deleted file mode 100644 index ac91792..0000000 --- a/pkg/github/actions/session.go +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package actions - -import ( - "context" - "fmt" - "net/http" - - "github.com/google/uuid" - "github.com/macstadium/orka-github-actions-integration/pkg/github/types" -) - -func (client *ActionsClient) CreateMessageSession(ctx context.Context, runnerScaleSetId int, owner string) (*types.RunnerScaleSetSession, error) { - path := fmt.Sprintf("/%s/%d/sessions", scaleSetEndpoint, runnerScaleSetId) - - newSession := &types.RunnerScaleSetSession{ - OwnerName: owner, - } - - return RequestJSON[types.RunnerScaleSetSession, types.RunnerScaleSetSession](ctx, client, http.MethodPost, path, newSession) -} - -func (client *ActionsClient) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*types.RunnerScaleSetSession, error) { - path := fmt.Sprintf("/%s/%d/sessions/%s", scaleSetEndpoint, runnerScaleSetId, sessionId.String()) - - return RequestJSON[types.RunnerScaleSetSession, types.RunnerScaleSetSession](ctx, client, http.MethodPatch, path, nil) -} - -func (client *ActionsClient) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error { - path := fmt.Sprintf("/%s/%d/sessions/%s", scaleSetEndpoint, runnerScaleSetId, sessionId.String()) - - _, err := RequestJSON[any, any](ctx, client, http.MethodDelete, path, nil) - - return err -} diff --git a/pkg/github/app/client.go b/pkg/github/app/client.go deleted file mode 100644 index 24bcafe..0000000 --- a/pkg/github/app/client.go +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package app - -import ( - "context" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/golang-jwt/jwt/v4" - "github.com/macstadium/orka-github-actions-integration/pkg/api" - "github.com/macstadium/orka-github-actions-integration/pkg/env" - "github.com/macstadium/orka-github-actions-integration/pkg/github/types" - retryablehttp "github.com/macstadium/orka-github-actions-integration/pkg/http" -) - -func FetchAccessToken(ctx context.Context, envData *env.Data) (*types.AccessToken, error) { - accessTokenJWT, err := createJWTForGitHubApp(envData.GitHubAppID, envData.GitHubAppPrivateKey) - if err != nil { - return nil, err - } - - httpClient, err := retryablehttp.NewClient(&retryablehttp.ClientTransport{ - Token: accessTokenJWT, - ContentType: "application/vnd.github+json", - }) - if err != nil { - return nil, err - } - - path := fmt.Sprintf("%s/app/installations/%v/access_tokens", envData.GitHubAPIUrl, envData.GitHubAppInstallationID) - - return api.RequestJSON[any, types.AccessToken](ctx, httpClient.Client, http.MethodPost, path, nil) -} - -func createJWTForGitHubApp(appID int64, privateKeyContent string) (string, error) { - // Encode as JWT - // See https://docs.github.com/en/developers/apps/building-github-apps/authenticating-with-github-apps#authenticating-as-a-github-app - - // Going back in time a bit helps with clock skew. - issuedAt := time.Now().Add(-60 * time.Second) - // Max expiration date is 10 minutes. - expiresAt := issuedAt.Add(9 * time.Minute) - claims := &jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(issuedAt), - ExpiresAt: jwt.NewNumericDate(expiresAt), - Issuer: strconv.FormatInt(appID, 10), - } - - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - - privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKeyContent)) - if err != nil { - return "", fmt.Errorf("error parsing PKCS#1 RSA private key: %v", err) - } - - return token.SignedString(privateKey) -} diff --git a/pkg/github/auth/client.go b/pkg/github/auth/client.go deleted file mode 100644 index e94513c..0000000 --- a/pkg/github/auth/client.go +++ /dev/null @@ -1,72 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package auth - -import ( - "context" - "fmt" - "net/http" - - "github.com/macstadium/orka-github-actions-integration/pkg/api" - "github.com/macstadium/orka-github-actions-integration/pkg/github" - "github.com/macstadium/orka-github-actions-integration/pkg/github/types" - retryablehttp "github.com/macstadium/orka-github-actions-integration/pkg/http" -) - -func GetAuthorizationInfo(ctx context.Context, accessToken *types.AccessToken, githubApiUrl string, config *github.GitHubConfig) (*types.AuthorizationInfo, error) { - registrationToken, err := getRegistrationToken(ctx, githubApiUrl, config, accessToken.Token) - if err != nil { - return nil, err - } - - path := fmt.Sprintf("%s/actions/runner-registration", githubApiUrl) - - body := &types.RegistrationPayload{ - Url: config.URL, - RunnerEvent: "register", - } - - httpClient, err := retryablehttp.NewClient(&retryablehttp.ClientTransport{ - ContentType: "application/json", - RemoteAuth: registrationToken.Token, - }) - if err != nil { - return nil, err - } - - return api.RequestJSON[types.RegistrationPayload, types.AuthorizationInfo](ctx, httpClient.Client, http.MethodPost, path, body) -} - -func createRegistrationTokenPath(githubApiUrl string, config *github.GitHubConfig) (string, error) { - switch config.Scope { - case github.GitHubScopeOrganization: - path := fmt.Sprintf("%s/orgs/%s/actions/runners/registration-token", githubApiUrl, config.Organization) - return path, nil - - case github.GitHubScopeRepository: - path := fmt.Sprintf("%s/repos/%s/%s/actions/runners/registration-token", githubApiUrl, config.Organization, config.Repository) - return path, nil - - default: - return "", fmt.Errorf("unknown scope for config url: %s", config.URL) - } -} - -func getRegistrationToken(ctx context.Context, githubApiUrl string, config *github.GitHubConfig, accessToken string) (*types.RegistrationToken, error) { - path, err := createRegistrationTokenPath(githubApiUrl, config) - if err != nil { - return nil, err - } - - httpClient, err := retryablehttp.NewClient(&retryablehttp.ClientTransport{ - Token: accessToken, - ContentType: "application/vnd.github.v3+json", - }) - if err != nil { - return nil, err - } - - return api.RequestJSON[any, types.RegistrationToken](ctx, httpClient.Client, http.MethodPost, path, nil) -} diff --git a/pkg/github/config.go b/pkg/github/config.go deleted file mode 100644 index 1bf0878..0000000 --- a/pkg/github/config.go +++ /dev/null @@ -1,62 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package github - -import ( - "fmt" - "net/url" - "strings" -) - -type GitHubConfig struct { - Scope GitHubScope - Organization string - Repository string - URL string -} - -type GitHubScope int - -const ( - GitHubScopeUnknown GitHubScope = iota - GitHubScopeOrganization - GitHubScopeRepository -) - -var ErrInvalidGitHubConfigURL = fmt.Errorf("invalid config URL, should point to an organization or repository") - -func NewGitHubConfig(gitHubURL string) (*GitHubConfig, error) { - u, err := url.Parse(strings.Trim(gitHubURL, "/")) - if err != nil { - return nil, err - } - - pathParts := strings.Split(strings.Trim(u.Path, "/"), "/") - - invalidURLError := fmt.Errorf("%q: %s", u.String(), ErrInvalidGitHubConfigURL) - - config := &GitHubConfig{ - URL: gitHubURL, - } - - switch len(pathParts) { - case 1: // Organization - if pathParts[0] == "" { - return nil, invalidURLError - } - - config.Scope = GitHubScopeOrganization - config.Organization = pathParts[0] - - case 2: // Repository - config.Scope = GitHubScopeRepository - config.Organization = pathParts[0] - config.Repository = pathParts[1] - default: - return nil, invalidURLError - } - - return config, nil -} diff --git a/pkg/github/errors/errors.go b/pkg/github/errors/errors.go deleted file mode 100644 index d198d89..0000000 --- a/pkg/github/errors/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package errors - -type MessageQueueTokenExpiredError struct { - Message string -} - -func (e *MessageQueueTokenExpiredError) Error() string { - return e.Message -} - -type HttpClientSideError struct { - msg string - Code int -} - -func (e *HttpClientSideError) Error() string { - return e.msg -} diff --git a/pkg/github/github_suite_test.go b/pkg/github/github_suite_test.go deleted file mode 100644 index 6663de6..0000000 --- a/pkg/github/github_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package github_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestGithub(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Github Suite") -} diff --git a/pkg/github/github_test.go b/pkg/github/github_test.go deleted file mode 100644 index e01ce8b..0000000 --- a/pkg/github/github_test.go +++ /dev/null @@ -1,100 +0,0 @@ -// Licensed under the Apache License, Version 2.0 -// Original work from the Actions Runner Controller (ARC) project -// See https://github.com/actions/actions-runner-controller - -package github_test - -import ( - "fmt" - "strings" - - "github.com/macstadium/orka-github-actions-integration/pkg/github" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("GitHub Config", func() { - Context("Create GitHub Config", func() { - - tests := []struct { - configURL string - expected *github.GitHubConfig - }{ - { - configURL: "https://github.com/org/repo", - expected: &github.GitHubConfig{ - Scope: github.GitHubScopeRepository, - Organization: "org", - Repository: "repo", - }, - }, - { - configURL: "https://github.com/org/repo/", - expected: &github.GitHubConfig{ - Scope: github.GitHubScopeRepository, - Organization: "org", - Repository: "repo", - }, - }, - { - configURL: "https://github.com/org", - expected: &github.GitHubConfig{ - Scope: github.GitHubScopeOrganization, - Organization: "org", - Repository: "", - }, - }, - { - configURL: "https://www.github.com/org", - expected: &github.GitHubConfig{ - Scope: github.GitHubScopeOrganization, - Organization: "org", - Repository: "", - }, - }, - { - configURL: "https://www.github.com/org/", - expected: &github.GitHubConfig{ - Scope: github.GitHubScopeOrganization, - Organization: "org", - Repository: "", - }, - }, - { - configURL: "https://github.localhost/org", - expected: &github.GitHubConfig{ - Scope: github.GitHubScopeOrganization, - Organization: "org", - Repository: "", - }, - }, - } - - for _, test := range tests { - It(fmt.Sprintf("Should create GitHub config with url %s", test.configURL), func() { - config, err := github.NewGitHubConfig(test.configURL) - - Expect(err).To(BeNil()) - - Expect(config.Scope).To(Equal(test.expected.Scope)) - Expect(config.Organization).To(Equal(test.expected.Organization)) - Expect(config.Repository).To(Equal(test.expected.Repository)) - }) - } - - invalidURLs := []string{ - "https://github.com/", - "https://github.com", - "https://github.com/some/random/path", - } - - for _, invalidURL := range invalidURLs { - It(fmt.Sprintf("Should fail to create GitHub config with url %s", invalidURL), func() { - config, err := github.NewGitHubConfig(invalidURL) - - Expect(config).To(BeNil()) - Expect(err.Error()).To(Equal(fmt.Sprintf("%q: invalid config URL, should point to an organization or repository", strings.Trim(invalidURL, "/")))) - }) - } - }) -}) diff --git a/pkg/github/messagequeue/manager.go b/pkg/github/messagequeue/manager.go index 6015524..5fdc309 100644 --- a/pkg/github/messagequeue/manager.go +++ b/pkg/github/messagequeue/manager.go @@ -6,12 +6,10 @@ package messagequeue import ( "context" - "errors" "fmt" "time" "github.com/macstadium/orka-github-actions-integration/pkg/github/actions" - ghErrors "github.com/macstadium/orka-github-actions-integration/pkg/github/errors" "github.com/macstadium/orka-github-actions-integration/pkg/github/types" "github.com/macstadium/orka-github-actions-integration/pkg/logging" ) @@ -26,80 +24,25 @@ func NewMessageQueueManager(client actions.ActionsService, session *types.Runner func (m *MessageQueueManager) ReceiveNextMessage(ctx context.Context, lastMessageId int64) (*types.RunnerScaleSetMessage, error) { message, err := m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId) - if err == nil { - return message, nil - } - - expiredError := &ghErrors.MessageQueueTokenExpiredError{} - if !errors.As(err, &expiredError) { - return nil, fmt.Errorf("get message failed. %w", err) - } - - m.logger.Info("message queue token is expired during GetNextMessage, refreshing...") - session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId) - if err != nil { - return nil, fmt.Errorf("unable to refresh message session. %w", err) - } - - m.session = session - message, err = m.client.GetMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, lastMessageId) if err != nil { - return nil, fmt.Errorf("delete message failed after refresh message session. %w", err) + return nil, fmt.Errorf("get message failed. %w", err) } return message, nil } func (m *MessageQueueManager) DeleteMessage(ctx context.Context, messageId int64) error { - err := m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId) - if err == nil { - return nil - } - - expiredError := &ghErrors.MessageQueueTokenExpiredError{} - if !errors.As(err, &expiredError) { + if err := m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId); err != nil { return fmt.Errorf("delete message failed. %w", err) } - m.logger.Info("message queue token is expired during DeleteMessage, refreshing...") - session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId) - if err != nil { - return fmt.Errorf("unable to refresh message session. %w", err) - } - - m.session = session - - err = m.client.DeleteMessage(ctx, m.session.MessageQueueUrl, m.session.MessageQueueAccessToken, messageId) - if err != nil { - return fmt.Errorf("delete message failed after refresh message session. %w", err) - } - return nil - } func (m *MessageQueueManager) AcquireJobs(ctx context.Context, requestIds []int64) ([]int64, error) { ids, err := m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds) - if err == nil { - return ids, nil - } - - expiredError := &ghErrors.MessageQueueTokenExpiredError{} - if !errors.As(err, &expiredError) { - return nil, fmt.Errorf("acquire jobs failed. %w", err) - } - - m.logger.Info("message queue token is expired during AcquireJobs, refreshing...") - session, err := m.client.RefreshMessageSession(ctx, m.session.RunnerScaleSet.Id, m.session.SessionId) if err != nil { - return nil, fmt.Errorf("unable to refresh message session. %w", err) - } - - m.session = session - - ids, err = m.client.AcquireJobs(ctx, m.session.RunnerScaleSet.Id, m.session.MessageQueueAccessToken, requestIds) - if err != nil { - return nil, fmt.Errorf("acquire jobs failed after refresh message session. %w", err) + return nil, fmt.Errorf("acquire jobs failed. %w", err) } return ids, nil diff --git a/pkg/github/runners/vm_tracker_test.go b/pkg/github/runners/vm_tracker_test.go index ddf39a3..ede9046 100644 --- a/pkg/github/runners/vm_tracker_test.go +++ b/pkg/github/runners/vm_tracker_test.go @@ -69,9 +69,6 @@ func (m *MockActionsClient) CreateMessageSession(ctx context.Context, id int, ow func (m *MockActionsClient) DeleteMessageSession(ctx context.Context, id int, sessionId *uuid.UUID) error { return nil } -func (m *MockActionsClient) RefreshMessageSession(ctx context.Context, id int, sessionId *uuid.UUID) (*types.RunnerScaleSetSession, error) { - return nil, nil -} func (m *MockActionsClient) AcquireJobs(ctx context.Context, id int, token string, reqIds []int64) ([]int64, error) { return nil, nil } diff --git a/pkg/github/scalesetclient/client.go b/pkg/github/scalesetclient/client.go index c56bffb..854ecc2 100644 --- a/pkg/github/scalesetclient/client.go +++ b/pkg/github/scalesetclient/client.go @@ -131,14 +131,6 @@ func (c *Client) CreateMessageSession(ctx context.Context, runnerScaleSetId int, return toSession(sessionClient.Session()), nil } -func (c *Client) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*types.RunnerScaleSetSession, error) { - session, err := c.currentSession() - if err != nil { - return nil, err - } - return toSession(session.Session()), nil -} - func (c *Client) DeleteMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) error { session, err := c.currentSession() if err != nil { diff --git a/pkg/http/logger.go b/pkg/http/logger.go deleted file mode 100644 index 13b819c..0000000 --- a/pkg/http/logger.go +++ /dev/null @@ -1,38 +0,0 @@ -package retryablehttp - -import ( - "context" - "errors" - - "go.uber.org/zap" -) - -type LeveledLogger struct { - logger *zap.SugaredLogger -} - -func (l *LeveledLogger) Error(msg string, keysAndValues ...interface{}) { - // go-retryablehttp logs every failed request at error level, including ones - // that fail purely because the caller's context was cancelled (e.g. during - // graceful shutdown). Downgrade those to debug to avoid alarming noise. - for i := 1; i < len(keysAndValues); i += 2 { - if err, ok := keysAndValues[i].(error); ok && errors.Is(err, context.Canceled) { - l.logger.Debugw(msg, keysAndValues...) - return - } - } - - l.logger.Errorw(msg, keysAndValues...) -} - -func (l *LeveledLogger) Info(msg string, keysAndValues ...interface{}) { - l.logger.Infow(msg, keysAndValues...) -} - -func (l *LeveledLogger) Debug(msg string, keysAndValues ...interface{}) { - l.logger.Debugw(msg, keysAndValues...) -} - -func (l *LeveledLogger) Warn(msg string, keysAndValues ...interface{}) { - l.logger.Warnw(msg, keysAndValues...) -} diff --git a/pkg/http/retryable-client.go b/pkg/http/retryable-client.go deleted file mode 100644 index fc42a04..0000000 --- a/pkg/http/retryable-client.go +++ /dev/null @@ -1,60 +0,0 @@ -package retryablehttp - -import ( - "fmt" - "net/http" - "time" - - retryable "github.com/hashicorp/go-retryablehttp" - "github.com/macstadium/orka-github-actions-integration/pkg/logging" -) - -type Client struct { - *http.Client - - retryMax int - retryWaitMax time.Duration -} - -type ClientTransport struct { - Token string - ContentType string - RemoteAuth string - Accept string -} - -func (t *ClientTransport) RoundTrip(req *http.Request) (*http.Response, error) { - authorization := fmt.Sprintf("Bearer %s", t.Token) - - if t.RemoteAuth != "" { - authorization = fmt.Sprintf("RemoteAuth %s", t.RemoteAuth) - } - - req.Header.Set("Authorization", authorization) - req.Header.Set("Content-Type", t.ContentType) - - if t.Accept != "" { - req.Header.Set("Accept", t.Accept) - } - - return http.DefaultTransport.RoundTrip(req) -} - -func NewClient(transport *ClientTransport) (*Client, error) { - client := &Client{ - retryMax: 4, - retryWaitMax: 30 * time.Second, - } - - retryClient := retryable.NewClient() - retryClient.Logger = &LeveledLogger{logger: logging.Logger.Named("http-client")} - - retryClient.RetryMax = client.retryMax - retryClient.RetryWaitMax = client.retryWaitMax - retryClient.HTTPClient.Timeout = 5 * time.Minute - - retryClient.HTTPClient.Transport = transport - client.Client = retryClient.StandardClient() - - return client, nil -} diff --git a/pkg/runner-provisioner/provisioner_test.go b/pkg/runner-provisioner/provisioner_test.go index 3caa0e9..a607041 100644 --- a/pkg/runner-provisioner/provisioner_test.go +++ b/pkg/runner-provisioner/provisioner_test.go @@ -68,10 +68,6 @@ func (m *MockActionsService) DeleteMessageSession(ctx context.Context, runnerSca return nil } -func (m *MockActionsService) RefreshMessageSession(ctx context.Context, runnerScaleSetId int, sessionId *uuid.UUID) (*types.RunnerScaleSetSession, error) { - return nil, nil -} - func (m *MockActionsService) AcquireJobs(ctx context.Context, runnerScaleSetId int, messageQueueAccessToken string, requestIds []int64) ([]int64, error) { return nil, nil } diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go index bd5ac09..6e07947 100644 --- a/pkg/utils/helpers.go +++ b/pkg/utils/helpers.go @@ -1,12 +1,5 @@ package utils -import ( - "fmt" - "time" - - "github.com/golang-jwt/jwt/v4" -) - func Map[T, V any](in []T, fn func(T) V) []V { out := make([]V, len(in)) for i, t := range in { @@ -14,23 +7,3 @@ func Map[T, V any](in []T, fn func(T) V) []V { } return out } - -func GetTokenExpirationTime(jwtToken string) (time.Time, error) { - type JwtClaims struct { - jwt.RegisteredClaims - } - token, _, err := jwt.NewParser().ParseUnverified(jwtToken, &JwtClaims{}) - if err != nil { - return time.Time{}, fmt.Errorf("failed to parse jwt token: %w", err) - } - - if claims, ok := token.Claims.(*JwtClaims); ok { - if claims.ExpiresAt != nil { - return claims.ExpiresAt.Time, nil - } else { - return time.Time{}, fmt.Errorf("missing expiration claim in token") - } - } - - return time.Time{}, fmt.Errorf("failed to parse token claims to get expire at") -} diff --git a/pkg/utils/helpers_test.go b/pkg/utils/helpers_test.go index db30d8e..8c769c6 100644 --- a/pkg/utils/helpers_test.go +++ b/pkg/utils/helpers_test.go @@ -1,7 +1,7 @@ package utils_test import ( - "time" + "strconv" "github.com/macstadium/orka-github-actions-integration/pkg/utils" . "github.com/onsi/ginkgo/v2" @@ -9,30 +9,13 @@ import ( ) var _ = Describe("Utils tests", func() { - Context("GetTokenExpirationTime", func() { - It("should correctly extract expiration time", func() { - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTA3MDI3NDUsInVzZXJuYW1lIjoiYWRtaW4ifQ.s56k7k89uI3jJ245jB8kF1234567890" - expiration, err := utils.GetTokenExpirationTime(token) - - Expect(err).To(BeNil()) - Expect(expiration).Should(BeTemporally("<", time.Now())) - }) - - It("should return error for a token with missing expiration claim", func() { - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.s56k7k89uI3jJ245jB8kF1234567890" - - _, err := utils.GetTokenExpirationTime(token) - - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("missing expiration claim in token")) + Context("Map", func() { + It("should apply the function to every element", func() { + Expect(utils.Map([]int{1, 2, 3}, strconv.Itoa)).To(Equal([]string{"1", "2", "3"})) }) - It("should return error for invalid token", func() { - token := "invalid-token" - - _, err := utils.GetTokenExpirationTime(token) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal("failed to parse jwt token: token contains an invalid number of segments")) + It("should return an empty slice for an empty input", func() { + Expect(utils.Map([]int{}, strconv.Itoa)).To(BeEmpty()) }) }) })