When updating credentials on an entity, we must ensure that the new credentials belong to the same endpoint as the entity. When an entity is created, the endpoint is determined by the credentials that were used during the create operation. From that point forward the entity is associated with an endpoint, and that cannot change. Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
457 lines
15 KiB
Go
457 lines
15 KiB
Go
// Copyright 2022 Cloudbase Solutions SRL
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License. You may obtain
|
|
// a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
// License for the specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package util
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/google/go-github/v57/github"
|
|
"github.com/pkg/errors"
|
|
|
|
runnerErrors "github.com/cloudbase/garm-provider-common/errors"
|
|
"github.com/cloudbase/garm/metrics"
|
|
"github.com/cloudbase/garm/params"
|
|
"github.com/cloudbase/garm/runner/common"
|
|
)
|
|
|
|
type githubClient struct {
|
|
*github.ActionsService
|
|
org *github.OrganizationsService
|
|
repo *github.RepositoriesService
|
|
enterprise *github.EnterpriseService
|
|
|
|
entity params.GithubEntity
|
|
}
|
|
|
|
func (g *githubClient) ListEntityHooks(ctx context.Context, opts *github.ListOptions) (ret []*github.Hook, response *github.Response, err error) {
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"ListHooks", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"ListHooks", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, response, err = g.repo.ListHooks(ctx, g.entity.Owner, g.entity.Name, opts)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, response, err = g.org.ListHooks(ctx, g.entity.Owner, opts)
|
|
default:
|
|
return nil, nil, fmt.Errorf("invalid entity type: %s", g.entity.EntityType)
|
|
}
|
|
return ret, response, err
|
|
}
|
|
|
|
func (g *githubClient) GetEntityHook(ctx context.Context, id int64) (ret *github.Hook, err error) {
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"GetHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"GetHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, _, err = g.repo.GetHook(ctx, g.entity.Owner, g.entity.Name, id)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, _, err = g.org.GetHook(ctx, g.entity.Owner, id)
|
|
default:
|
|
return nil, errors.New("invalid entity type")
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func (g *githubClient) CreateEntityHook(ctx context.Context, hook *github.Hook) (ret *github.Hook, err error) {
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"CreateHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"CreateHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, _, err = g.repo.CreateHook(ctx, g.entity.Owner, g.entity.Name, hook)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, _, err = g.org.CreateHook(ctx, g.entity.Owner, hook)
|
|
default:
|
|
return nil, errors.New("invalid entity type")
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func (g *githubClient) DeleteEntityHook(ctx context.Context, id int64) (ret *github.Response, err error) {
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"DeleteHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"DeleteHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, err = g.repo.DeleteHook(ctx, g.entity.Owner, g.entity.Name, id)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, err = g.org.DeleteHook(ctx, g.entity.Owner, id)
|
|
default:
|
|
return nil, errors.New("invalid entity type")
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func (g *githubClient) PingEntityHook(ctx context.Context, id int64) (ret *github.Response, err error) {
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"PingHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"PingHook", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, err = g.repo.PingHook(ctx, g.entity.Owner, g.entity.Name, id)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, err = g.org.PingHook(ctx, g.entity.Owner, id)
|
|
default:
|
|
return nil, errors.New("invalid entity type")
|
|
}
|
|
return ret, err
|
|
}
|
|
|
|
func (g *githubClient) ListEntityRunners(ctx context.Context, opts *github.ListOptions) (*github.Runners, *github.Response, error) {
|
|
var ret *github.Runners
|
|
var response *github.Response
|
|
var err error
|
|
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"ListEntityRunners", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"ListEntityRunners", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, response, err = g.ListRunners(ctx, g.entity.Owner, g.entity.Name, opts)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, response, err = g.ListOrganizationRunners(ctx, g.entity.Owner, opts)
|
|
case params.GithubEntityTypeEnterprise:
|
|
ret, response, err = g.enterprise.ListRunners(ctx, g.entity.Owner, opts)
|
|
default:
|
|
return nil, nil, errors.New("invalid entity type")
|
|
}
|
|
|
|
return ret, response, err
|
|
}
|
|
|
|
func (g *githubClient) ListEntityRunnerApplicationDownloads(ctx context.Context) ([]*github.RunnerApplicationDownload, *github.Response, error) {
|
|
var ret []*github.RunnerApplicationDownload
|
|
var response *github.Response
|
|
var err error
|
|
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"ListEntityRunnerApplicationDownloads", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"ListEntityRunnerApplicationDownloads", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, response, err = g.ListRunnerApplicationDownloads(ctx, g.entity.Owner, g.entity.Name)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, response, err = g.ListOrganizationRunnerApplicationDownloads(ctx, g.entity.Owner)
|
|
case params.GithubEntityTypeEnterprise:
|
|
ret, response, err = g.enterprise.ListRunnerApplicationDownloads(ctx, g.entity.Owner)
|
|
default:
|
|
return nil, nil, errors.New("invalid entity type")
|
|
}
|
|
|
|
return ret, response, err
|
|
}
|
|
|
|
func (g *githubClient) RemoveEntityRunner(ctx context.Context, runnerID int64) (*github.Response, error) {
|
|
var response *github.Response
|
|
var err error
|
|
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"RemoveEntityRunner", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"RemoveEntityRunner", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
response, err = g.RemoveRunner(ctx, g.entity.Owner, g.entity.Name, runnerID)
|
|
case params.GithubEntityTypeOrganization:
|
|
response, err = g.RemoveOrganizationRunner(ctx, g.entity.Owner, runnerID)
|
|
case params.GithubEntityTypeEnterprise:
|
|
response, err = g.enterprise.RemoveRunner(ctx, g.entity.Owner, runnerID)
|
|
default:
|
|
return nil, errors.New("invalid entity type")
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
|
|
func (g *githubClient) CreateEntityRegistrationToken(ctx context.Context) (*github.RegistrationToken, *github.Response, error) {
|
|
var ret *github.RegistrationToken
|
|
var response *github.Response
|
|
var err error
|
|
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"CreateEntityRegistrationToken", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
defer func() {
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"CreateEntityRegistrationToken", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
}
|
|
}()
|
|
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, response, err = g.CreateRegistrationToken(ctx, g.entity.Owner, g.entity.Name)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, response, err = g.CreateOrganizationRegistrationToken(ctx, g.entity.Owner)
|
|
case params.GithubEntityTypeEnterprise:
|
|
ret, response, err = g.enterprise.CreateRegistrationToken(ctx, g.entity.Owner)
|
|
default:
|
|
return nil, nil, errors.New("invalid entity type")
|
|
}
|
|
|
|
return ret, response, err
|
|
}
|
|
|
|
func (g *githubClient) getOrganizationRunnerGroupIDByName(ctx context.Context, entity params.GithubEntity, rgName string) (int64, error) {
|
|
opts := github.ListOrgRunnerGroupOptions{
|
|
ListOptions: github.ListOptions{
|
|
PerPage: 100,
|
|
},
|
|
}
|
|
|
|
for {
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"ListOrganizationRunnerGroups", // label: operation
|
|
entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
runnerGroups, ghResp, err := g.ListOrganizationRunnerGroups(ctx, entity.Owner, &opts)
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"ListOrganizationRunnerGroups", // label: operation
|
|
entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized {
|
|
return 0, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners")
|
|
}
|
|
return 0, errors.Wrap(err, "fetching runners")
|
|
}
|
|
for _, runnerGroup := range runnerGroups.RunnerGroups {
|
|
if runnerGroup.Name != nil && *runnerGroup.Name == rgName {
|
|
return *runnerGroup.ID, nil
|
|
}
|
|
}
|
|
if ghResp.NextPage == 0 {
|
|
break
|
|
}
|
|
opts.Page = ghResp.NextPage
|
|
}
|
|
return 0, runnerErrors.NewNotFoundError("runner group %s not found", rgName)
|
|
}
|
|
|
|
func (g *githubClient) getEnterpriseRunnerGroupIDByName(ctx context.Context, entity params.GithubEntity, rgName string) (int64, error) {
|
|
opts := github.ListEnterpriseRunnerGroupOptions{
|
|
ListOptions: github.ListOptions{
|
|
PerPage: 100,
|
|
},
|
|
}
|
|
|
|
for {
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"ListRunnerGroups", // label: operation
|
|
entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
runnerGroups, ghResp, err := g.enterprise.ListRunnerGroups(ctx, entity.Owner, &opts)
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"ListRunnerGroups", // label: operation
|
|
entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
if ghResp != nil && ghResp.StatusCode == http.StatusUnauthorized {
|
|
return 0, errors.Wrap(runnerErrors.ErrUnauthorized, "fetching runners")
|
|
}
|
|
return 0, errors.Wrap(err, "fetching runners")
|
|
}
|
|
for _, runnerGroup := range runnerGroups.RunnerGroups {
|
|
if runnerGroup.Name != nil && *runnerGroup.Name == rgName {
|
|
return *runnerGroup.ID, nil
|
|
}
|
|
}
|
|
if ghResp.NextPage == 0 {
|
|
break
|
|
}
|
|
opts.Page = ghResp.NextPage
|
|
}
|
|
return 0, runnerErrors.NewNotFoundError("runner group not found")
|
|
}
|
|
|
|
func (g *githubClient) GetEntityJITConfig(ctx context.Context, instance string, pool params.Pool, labels []string) (jitConfigMap map[string]string, runner *github.Runner, err error) {
|
|
// If no runner group is set, use the default runner group ID. This is also the default for
|
|
// repository level runners.
|
|
var rgID int64 = 1
|
|
|
|
if pool.GitHubRunnerGroup != "" {
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeOrganization:
|
|
rgID, err = g.getOrganizationRunnerGroupIDByName(ctx, g.entity, pool.GitHubRunnerGroup)
|
|
case params.GithubEntityTypeEnterprise:
|
|
rgID, err = g.getEnterpriseRunnerGroupIDByName(ctx, g.entity, pool.GitHubRunnerGroup)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("getting runner group ID: %w", err)
|
|
}
|
|
}
|
|
|
|
req := github.GenerateJITConfigRequest{
|
|
Name: instance,
|
|
RunnerGroupID: rgID,
|
|
Labels: labels,
|
|
// nolint:golangci-lint,godox
|
|
// TODO(gabriel-samfira): Should we make this configurable?
|
|
WorkFolder: github.String("_work"),
|
|
}
|
|
|
|
metrics.GithubOperationCount.WithLabelValues(
|
|
"GetEntityJITConfig", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
|
|
var ret *github.JITRunnerConfig
|
|
var response *github.Response
|
|
|
|
switch g.entity.EntityType {
|
|
case params.GithubEntityTypeRepository:
|
|
ret, response, err = g.GenerateRepoJITConfig(ctx, g.entity.Owner, g.entity.Name, &req)
|
|
case params.GithubEntityTypeOrganization:
|
|
ret, response, err = g.GenerateOrgJITConfig(ctx, g.entity.Owner, &req)
|
|
case params.GithubEntityTypeEnterprise:
|
|
ret, response, err = g.enterprise.GenerateEnterpriseJITConfig(ctx, g.entity.Owner, &req)
|
|
}
|
|
if err != nil {
|
|
metrics.GithubOperationFailedCount.WithLabelValues(
|
|
"GetEntityJITConfig", // label: operation
|
|
g.entity.LabelScope(), // label: scope
|
|
).Inc()
|
|
if response != nil && response.StatusCode == http.StatusUnauthorized {
|
|
return nil, nil, fmt.Errorf("failed to get JIT config: %w", err)
|
|
}
|
|
return nil, nil, fmt.Errorf("failed to get JIT config: %w", err)
|
|
}
|
|
|
|
defer func(run *github.Runner) {
|
|
if err != nil && run != nil {
|
|
_, innerErr := g.RemoveEntityRunner(ctx, run.GetID())
|
|
slog.With(slog.Any("error", innerErr)).ErrorContext(
|
|
ctx, "failed to remove runner",
|
|
"runner_id", run.GetID(), string(g.entity.EntityType), g.entity.String())
|
|
}
|
|
}(ret.Runner)
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(*ret.EncodedJITConfig)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to decode JIT config: %w", err)
|
|
}
|
|
|
|
var jitConfig map[string]string
|
|
if err := json.Unmarshal(decoded, &jitConfig); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to unmarshal JIT config: %w", err)
|
|
}
|
|
|
|
return jitConfig, ret.Runner, nil
|
|
}
|
|
|
|
func GithubClient(ctx context.Context, entity params.GithubEntity, credsDetails params.GithubCredentials) (common.GithubClient, error) {
|
|
httpClient, err := credsDetails.GetHTTPClient(ctx)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "fetching http client")
|
|
}
|
|
|
|
ghClient, err := github.NewClient(httpClient).WithEnterpriseURLs(credsDetails.APIBaseURL, credsDetails.UploadBaseURL)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "fetching github client")
|
|
}
|
|
|
|
cli := &githubClient{
|
|
ActionsService: ghClient.Actions,
|
|
org: ghClient.Organizations,
|
|
repo: ghClient.Repositories,
|
|
enterprise: ghClient.Enterprise,
|
|
entity: entity,
|
|
}
|
|
return cli, nil
|
|
}
|