Skip to content

Commit

Permalink
feat: refacto github/gitlab clients
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasMrqes committed Nov 27, 2024
1 parent d1a7ba6 commit 5a0a692
Show file tree
Hide file tree
Showing 21 changed files with 500 additions and 224 deletions.
2 changes: 0 additions & 2 deletions api/v1alpha1/terraformpullrequest_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (

// TerraformPullRequestSpec defines the desired state of TerraformPullRequest
type TerraformPullRequestSpec struct {
Provider string `json:"provider,omitempty"`
Branch string `json:"branch,omitempty"`
Base string `json:"base,omitempty"`
ID string `json:"id,omitempty"`
Expand All @@ -45,7 +44,6 @@ type TerraformPullRequestStatus struct {
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="ID",type=string,JSONPath=`.spec.id`
// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.status.state`
// +kubebuilder:printcolumn:name="Provider",type=string,JSONPath=`.spec.provider`
// +kubebuilder:printcolumn:name="Base",type=string,JSONPath=`.spec.base`
// +kubebuilder:printcolumn:name="Branch",type=string,JSONPath=`.spec.branch`
// TerraformPullRequest is the Schema for the TerraformPullRequests API
Expand Down
28 changes: 28 additions & 0 deletions deploy/charts/burrito/templates/tenant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,34 @@ subjects:
name: {{ $serviceAccount.name }}
namespace: {{ $tenant.namespace.name }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-access
labels:
app: burrito
namespace: {{ $tenant.namespace.name }}
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: burrito-server-secret-access
labels:
app: burrito
namespace: {{ $tenant.namespace.name }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: secret-access
subjects:
- kind: ServiceAccount
name: burrito-server
namespace: {{ $.Release.Namespace }}
---
{{- range $additionalRoleBinding := $serviceAccount.additionalRoleBindings }}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
Expand Down
11 changes: 8 additions & 3 deletions internal/burrito/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,14 @@ type ControllerTimers struct {
}

type RepositoryConfig struct {
SSHPrivateKey string `mapstructure:"sshPrivateKey"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
SSHPrivateKey string `mapstructure:"sshPrivateKey"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
GithubAppId int64 `mapstructure:"githubAppId"`
GithubAppInstallationId int64 `mapstructure:"githubAppInstallationId"`
GithubAppPrivateKey string `mapstructure:"githubAppPrivateKey"`
GithubToken string `mapstructure:"githubToken"`
GitlabToken string `mapstructure:"gitlabToken"`
}

type RunnerConfig struct {
Expand Down
84 changes: 66 additions & 18 deletions internal/controllers/terraformpullrequest/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ package terraformpullrequest
import (
"context"
"fmt"
"strings"

"github.com/google/go-cmp/cmp"
"github.com/padok-team/burrito/internal/burrito/config"
"github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment"
"github.com/padok-team/burrito/internal/controllers/terraformpullrequest/github"
datastore "github.com/padok-team/burrito/internal/datastore/client"

"github.com/padok-team/burrito/internal/controllers/terraformpullrequest/gitlab"
datastore "github.com/padok-team/burrito/internal/datastore/client"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -29,8 +27,7 @@ import (
)

type Provider interface {
Init(*config.Config) error
IsFromProvider(*configv1alpha1.TerraformPullRequest) bool
Init() error
GetChanges(*configv1alpha1.TerraformRepository, *configv1alpha1.TerraformPullRequest) ([]string, error)
Comment(*configv1alpha1.TerraformRepository, *configv1alpha1.TerraformPullRequest, comment.Comment) error
}
Expand All @@ -40,7 +37,7 @@ type Reconciler struct {
client.Client
Scheme *runtime.Scheme
Config *config.Config
Providers []Provider
Providers map[string]Provider
Recorder record.EventRecorder
Datastore datastore.Client
}
Expand Down Expand Up @@ -84,6 +81,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
log.Errorf("failed to get TerraformRepository: %s", err)
return ctrl.Result{}, err
}
if _, ok := r.Providers[fmt.Sprintf("%s/%s", repository.Namespace, repository.Name)]; !ok {
log.Infof("initializing provider for repository %s/%s", repository.Namespace, repository.Name)
r.Providers[fmt.Sprintf("%s/%s", repository.Namespace, repository.Name)], err = r.initializeProvider(ctx, repository)
if err != nil {
log.Errorf("could not initialize provider for repository %s: %s", repository.Name, err)
return ctrl.Result{}, err
}
}
state := r.GetState(ctx, pr)
result := state.Handler(ctx, r, repository, pr)
pr.Status = state.Status
Expand All @@ -98,25 +103,23 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
providers := []Provider{}
for _, p := range []Provider{&github.Github{}, &gitlab.Gitlab{}} {
name := strings.Split(fmt.Sprintf("%T", p), ".")
err := p.Init(r.Config)
if err != nil {
log.Warnf("could not initialize provider %s: %s", name, err)
continue
}
log.Infof("provider %s successfully initialized", name)
providers = append(providers, p)
}
r.Providers = providers
r.Providers = make(map[string]Provider)
return ctrl.NewControllerManagedBy(mgr).
For(&configv1alpha1.TerraformPullRequest{}).
WithOptions(controller.Options{MaxConcurrentReconciles: r.Config.Controller.MaxConcurrentReconciles}).
WithEventFilter(ignorePredicate()).
Complete(r)
}

func GetProviderForPullRequest(pr *configv1alpha1.TerraformPullRequest, r *Reconciler) (Provider, error) {
for key, p := range r.Providers {
if fmt.Sprintf("%s/%s", pr.Spec.Repository.Namespace, pr.Spec.Repository.Name) == key {
return p, nil
}
}
return nil, fmt.Errorf("no provider found for pull request %s", pr.Name)
}

func ignorePredicate() predicate.Predicate {
return predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
Expand All @@ -134,3 +137,48 @@ func ignorePredicate() predicate.Predicate {
},
}
}

func (r *Reconciler) initializeProvider(ctx context.Context, repository *configv1alpha1.TerraformRepository) (Provider, error) {
secret := &corev1.Secret{}
err := r.Client.Get(ctx, types.NamespacedName{
Name: repository.Spec.Repository.SecretName,
Namespace: repository.Namespace,
}, secret)
if err != nil {
log.Errorf("failed to get credentials secret for repository %s: %s", repository.Name, err)
return nil, err
}
var provider Provider

if repository.Spec.Repository.Url == "" {
return nil, fmt.Errorf("no repository URL found in TerraformRepository.spec.repository.url, %s", repository.Name)
}

if secret.Data["githubAppId"] != nil && secret.Data["githubAppInstallationId"] != nil && secret.Data["githubAppPrivateKey"] != nil {
provider = &github.Github{
AppId: string(secret.Data["githubAppId"]),
AppInstallationId: string(secret.Data["githubAppInstallationId"]),
AppPrivateKey: string(secret.Data["githubAppPrivateKey"]),
Url: repository.Spec.Repository.Url,
}
} else if secret.Data["githubToken"] != nil {
provider = &github.Github{
ApiToken: string(secret.Data["githubToken"]),
Url: repository.Spec.Repository.Url,
}
} else if secret.Data["gitlabToken"] != nil {
provider = &gitlab.Gitlab{
ApiToken: string(secret.Data["gitlabToken"]),
Url: repository.Spec.Repository.Url,
}
} else {
return nil, fmt.Errorf("no valid provider credentials found in secret. %s Please provide at least one of the following: <githubAppId, githubAppInstallationId, githubAppPrivateKey>, <githubToken>, <gitlabToken> in the secret referenced in TerraformRepository.spec.repository.secretName, %s", repository.Spec.Repository.SecretName)

Check failure on line 175 in internal/controllers/terraformpullrequest/controller.go

View workflow job for this annotation

GitHub Actions / Lint Go

printf: fmt.Errorf format %s reads arg #2, but call has 1 arg (govet)

Check failure on line 175 in internal/controllers/terraformpullrequest/controller.go

View workflow job for this annotation

GitHub Actions / Unit Tests

fmt.Errorf format %s reads arg #2, but call has 1 arg
}

err = provider.Init()
if err != nil {
log.Errorf("failed to initialize provider for repository %s: %s", repository.Name, err)
return nil, err
}
return provider, nil
}
4 changes: 2 additions & 2 deletions internal/controllers/terraformpullrequest/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ var _ = BeforeSuite(func() {
Config: config.TestConfig(),
Scheme: scheme.Scheme,
Datastore: datastore.NewMockClient(),
Providers: []controller.Provider{
&provider.Mock{},
Providers: map[string]controller.Provider{
"mock": &provider.Mock{},
},
Recorder: record.NewBroadcasterForTests(1*time.Second).NewRecorder(scheme.Scheme, corev1.EventSource{
Component: "burrito",
Expand Down
81 changes: 60 additions & 21 deletions internal/controllers/terraformpullrequest/github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ package github
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v66/github"
configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1"
"github.com/padok-team/burrito/internal/annotations"
"github.com/padok-team/burrito/internal/burrito/config"
"github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment"
utils "github.com/padok-team/burrito/internal/utils/url"
log "github.com/sirupsen/logrus"
Expand All @@ -20,43 +21,65 @@ import (

type Github struct {
*github.Client
AppId string
AppInstallationId string
AppPrivateKey string
ApiToken string
Url string
}

func (g *Github) IsAppConfigPresent(c *config.Config) bool {
return c.Controller.GithubConfig.AppId != 0 && c.Controller.GithubConfig.InstallationId != 0 && len(c.Controller.GithubConfig.PrivateKey) != 0
}
type GitHubSubscription string

const (
GitHubEnterprise GitHubSubscription = "enterprise"
GitHubClassic GitHubSubscription = "classic"
)

func (g *Github) IsAPITokenConfigPresent(c *config.Config) bool {
return len(c.Controller.GithubConfig.APIToken) != 0
func (g *Github) IsAppConfigPresent() bool {
return g.AppId != "" && g.AppInstallationId != "" && g.AppPrivateKey != ""
}

func (g *Github) Init(c *config.Config) error {
if g.IsAppConfigPresent(c) {
itr, err := ghinstallation.New(http.DefaultTransport, c.Controller.GithubConfig.AppId, c.Controller.GithubConfig.InstallationId, []byte(c.Controller.GithubConfig.PrivateKey))
func (g *Github) IsAPITokenConfigPresent() bool {
return g.ApiToken != ""
}

func (g *Github) Init() error {
apiUrl, subscription, err := inferBaseURL(g.Url)

Check failure on line 47 in internal/controllers/terraformpullrequest/github/provider.go

View workflow job for this annotation

GitHub Actions / Lint Go

SA4006: this value of `err` is never used (staticcheck)
httpClient := &http.Client{}
if g.IsAppConfigPresent() {
appId, err := strconv.ParseInt(g.AppId, 10, 64)
if err != nil {
return errors.New("error while parsing github app id: " + err.Error())
}
appInstallationId, err := strconv.ParseInt(g.AppInstallationId, 10, 64)
if err != nil {
return errors.New("error while parsing github app installation id: " + err.Error())
}
itr, err := ghinstallation.New(http.DefaultTransport, appId, appInstallationId, []byte(g.AppPrivateKey))
if err != nil {
return errors.New("error while creating github installation client: " + err.Error())
}

g.Client = github.NewClient(&http.Client{Transport: itr})
return nil
} else if g.IsAPITokenConfigPresent(c) {
httpClient.Transport = itr
} else if g.IsAPITokenConfigPresent() {
ctx := context.Background()

ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: c.Controller.GithubConfig.APIToken},
&oauth2.Token{AccessToken: g.ApiToken},
)
tc := oauth2.NewClient(ctx, ts)

g.Client = github.NewClient(tc)
return nil
httpClient = oauth2.NewClient(ctx, ts)
} else {
return errors.New("github config is not present")
}
}

func (g *Github) IsFromProvider(pr *configv1alpha1.TerraformPullRequest) bool {
return pr.Spec.Provider == "github"
if subscription == GitHubEnterprise {
g.Client, err = github.NewClient(httpClient).WithEnterpriseURLs(apiUrl, apiUrl)
if err != nil {
return errors.New("error while creating github enterprise client: " + err.Error())
}
} else if subscription == GitHubClassic {
g.Client = github.NewClient(httpClient)
}
return nil
}

func (g *Github) GetChanges(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ([]string, error) {
Expand Down Expand Up @@ -115,3 +138,19 @@ func parseGithubUrl(url string) (string, string) {
split := strings.Split(normalizedUrl[8:], "/")
return split[1], split[2]
}

func inferBaseURL(repoURL string) (string, GitHubSubscription, error) {
parsedURL, err := url.Parse(repoURL)
if err != nil {
return "", "", fmt.Errorf("invalid repository URL: %w", err)
}

host := parsedURL.Host
host = strings.TrimPrefix(host, "www.")

if host != "github.com" {
return fmt.Sprintf("https://%s/api/v3", host), GitHubEnterprise, nil
} else {
return "", GitHubClassic, nil
}
}
Loading

0 comments on commit 5a0a692

Please sign in to comment.