Skip to content

Commit

Permalink
Merge pull request #257 from gbenhaim/webhook-target
Browse files Browse the repository at this point in the history
RHTAPSRE-469: Webhook configuration based on repo url
  • Loading branch information
gbenhaim authored Mar 24, 2024
2 parents d4d8770 + 61a37d0 commit 7d2450d
Show file tree
Hide file tree
Showing 6 changed files with 279 additions and 13 deletions.
8 changes: 5 additions & 3 deletions controllers/component_build_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
appstudiov1alpha1 "github.com/redhat-appstudio/application-api/api/v1alpha1"
"github.com/redhat-appstudio/build-service/pkg/boerrors"
l "github.com/redhat-appstudio/build-service/pkg/logs"
"github.com/redhat-appstudio/build-service/pkg/webhook"
)

const (
Expand Down Expand Up @@ -190,9 +191,10 @@ type PaCBuildStatus struct {
// provision Pipelines as Code configuration for the Component or
// submit initial builds and dependent resources if PaC is not configured.
type ComponentBuildReconciler struct {
Client client.Client
Scheme *runtime.Scheme
EventRecorder record.EventRecorder
Client client.Client
Scheme *runtime.Scheme
EventRecorder record.EventRecorder
WebhookURLLoader webhook.WebhookURLLoader
}

// SetupWithManager sets up the controller with the Manager.
Expand Down
13 changes: 9 additions & 4 deletions controllers/component_build_controller_pac.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func (r *ComponentBuildReconciler) ProvisionPaCForComponent(ctx context.Context,
}

// Obtain Pipelines as Code callback URL
webhookTargetUrl, err = r.getPaCWebhookTargetUrl(ctx)
webhookTargetUrl, err = r.getPaCWebhookTargetUrl(ctx, component.Spec.Source.GitSource.URL)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -281,7 +281,7 @@ func (r *ComponentBuildReconciler) TriggerPaCBuild(ctx context.Context, componen
return true, nil
}

webhookTargetUrl, err := r.getPaCWebhookTargetUrl(ctx)
webhookTargetUrl, err := r.getPaCWebhookTargetUrl(ctx, component.Spec.Source.GitSource.URL)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -430,7 +430,7 @@ func (r *ComponentBuildReconciler) UndoPaCProvisionForComponent(ctx context.Cont

webhookTargetUrl := ""
if !gitops.IsPaCApplicationConfigured(gitProvider, pacSecret.Data) {
webhookTargetUrl, err = r.getPaCWebhookTargetUrl(ctx)
webhookTargetUrl, err = r.getPaCWebhookTargetUrl(ctx, component.Spec.Source.GitSource.URL)
if err != nil {
// Just log the error and continue with pruning merge request creation
log.Error(err, "failed to get Pipelines as Code webhook target URL. Webhook will not be deleted.", l.Action, l.ActionView, l.Audit, "true")
Expand Down Expand Up @@ -571,8 +571,13 @@ func generatePaCWebhookSecretString() string {
}

// getPaCWebhookTargetUrl returns URL to which events from git repository should be sent.
func (r *ComponentBuildReconciler) getPaCWebhookTargetUrl(ctx context.Context) (string, error) {
func (r *ComponentBuildReconciler) getPaCWebhookTargetUrl(ctx context.Context, repositoryURL string) (string, error) {
webhookTargetUrl := os.Getenv(pipelinesAsCodeRouteEnvVar)

if webhookTargetUrl == "" {
webhookTargetUrl = r.WebhookURLLoader.Load(repositoryURL)
}

if webhookTargetUrl == "" {
// The env variable is not set
// Use the installed on the cluster Pipelines as Code
Expand Down
11 changes: 8 additions & 3 deletions controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import (
releaseapi "github.com/redhat-appstudio/release-service/api/v1alpha1"

appstudioredhatcomv1alpha1 "github.com/redhat-appstudio/build-service/api/v1alpha1"
"github.com/redhat-appstudio/build-service/pkg/webhook"
//+kubebuilder:scaffold:imports
)

Expand Down Expand Up @@ -149,10 +150,14 @@ var _ = BeforeSuite(func() {
})
Expect(err).ToNot(HaveOccurred())

webhookConfig, err := webhook.LoadMappingFromFile("", os.ReadFile)
Expect(err).ToNot(HaveOccurred())

err = (&ComponentBuildReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
EventRecorder: k8sManager.GetEventRecorderFor("ComponentOnboarding"),
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
EventRecorder: k8sManager.GetEventRecorderFor("ComponentOnboarding"),
WebhookURLLoader: webhook.NewConfigWebhookURLLoader(webhookConfig),
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

Expand Down
21 changes: 18 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import (
appstudioredhatcomv1alpha1 "github.com/redhat-appstudio/build-service/api/v1alpha1"
"github.com/redhat-appstudio/build-service/controllers"
l "github.com/redhat-appstudio/build-service/pkg/logs"
"github.com/redhat-appstudio/build-service/pkg/webhook"
//+kubebuilder:scaffold:imports
)

Expand All @@ -78,11 +79,18 @@ func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var webhookConfigPath string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.StringVar(
&webhookConfigPath,
"webhook-config-path",
"",
"Path to a file that contains webhook configurations",
)

zapOpts := zap.Options{
TimeEncoder: uberzapcore.ISO8601TimeEncoder,
Expand Down Expand Up @@ -143,10 +151,17 @@ func main() {
os.Exit(1)
}

webhookConfig, err := webhook.LoadMappingFromFile(webhookConfigPath, os.ReadFile)
if err != nil {
setupLog.Error(err, "Failed to load webhook config file", "path", webhookConfigPath)
os.Exit(1)
}

if err = (&controllers.ComponentBuildReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
EventRecorder: mgr.GetEventRecorderFor("ComponentOnboarding"),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
EventRecorder: mgr.GetEventRecorderFor("ComponentOnboarding"),
WebhookURLLoader: webhook.NewConfigWebhookURLLoader(webhookConfig),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ComponentOnboarding")
os.Exit(1)
Expand Down
77 changes: 77 additions & 0 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package webhook

import (
"encoding/json"
"strings"

"github.com/go-logr/logr"
ctrl "sigs.k8s.io/controller-runtime"
)

var log logr.Logger = ctrl.Log.WithName("webhook")

type WebhookURLLoader interface {
Load(repositoryUrl string) string
}

type ConfigWebhookURLLoader struct {
// Prefix to target url mapping
mapping map[string]string
}

func NewConfigWebhookURLLoader(mapping map[string]string) ConfigWebhookURLLoader {
return ConfigWebhookURLLoader{mapping: mapping}
}

/*
Load implements WebhookURLLoader.
Find the longest prefix match of `repositoryUrl“ and the keys of `mapping`,
and return the value of that key.
*/
func (c ConfigWebhookURLLoader) Load(repositoryUrl string) string {
longestPrefixLen := 0
matchedTarget := ""
for prefix, target := range c.mapping {
if strings.HasPrefix(repositoryUrl, prefix) && len(prefix) > longestPrefixLen {
longestPrefixLen = len(prefix)
matchedTarget = target
}
}

// Provide a default using the empty string
if matchedTarget == "" {
if val, ok := c.mapping[""]; ok {
matchedTarget = val
}
}

return matchedTarget
}

var _ WebhookURLLoader = ConfigWebhookURLLoader{}

type FileReader func(name string) ([]byte, error)

// Load the prefix to target url from a file
func LoadMappingFromFile(path string, fileReader FileReader) (map[string]string, error) {
if path == "" {
log.Info("Webhook config was not provided")
return map[string]string{}, nil
}

content, err := fileReader(path)
if err != nil {
return nil, err
}

var mapping map[string]string
err = json.Unmarshal(content, &mapping)
if err != nil {
return nil, err
}

log.Info("Using webhook config", "config", mapping)

return mapping, nil
}
162 changes: 162 additions & 0 deletions pkg/webhook/webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package webhook

import (
"errors"
"reflect"
"testing"
)

func TestConfigWebhookURLLoader_Load(t *testing.T) {
type fields struct {
mapping map[string]string
}
type args struct {
repositoryUrl string
}
tests := []struct {
name string
fields fields
args args
want string
}{
{
name: "No match should return an empty string",
fields: fields{
mapping: map[string]string{},
},
args: args{
repositoryUrl: "https://github.com/org/repo",
},
want: "",
},
{
name: "Multiple prefix' with an exact match",
fields: fields{
mapping: map[string]string{
"https://github.com/org/repo": "chosenTarget",
"https://github.com/org/": "otherTarget1",
"https://github.com/": "otherTarget2",
"https://gitlab.com/": "otherTarget3",
},
},
args: args{
repositoryUrl: "https://github.com/org/repo",
},
want: "chosenTarget",
},
{
name: "No exact match, the longest prefix is chosen",
fields: fields{
mapping: map[string]string{
"https://github.com/org/": "chosenTarget",
"https://github.com/": "otherTarget1",
"https://gitlab.com/": "otherTarget2",
},
},
args: args{
repositoryUrl: "https://github.com/org/repo",
},
want: "chosenTarget",
},
{
name: "Match on an empty string",
fields: fields{
mapping: map[string]string{
"": "chosenTarget",
"https://gitlab.com/": "otherTarget2",
},
},
args: args{
repositoryUrl: "https://github.com/org/repo",
},
want: "chosenTarget",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConfigWebhookURLLoader(tt.fields.mapping)
if got := c.Load(tt.args.repositoryUrl); got != tt.want {
t.Errorf("ConfigWebhookURLLoader.Load() = %v, want %v", got, tt.want)
}
})
}
}

func TestLoadMappingFromFile(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
fileReader FileReader
want map[string]string
wantErr bool
}{
{
name: "Load empty file",
args: args{path: "file"},
fileReader: func(name string) ([]byte, error) {
return []byte("{}"), nil
},
want: map[string]string{},
wantErr: false,
},
{
name: "Load non empty file",
args: args{path: "file"},
fileReader: func(name string) ([]byte, error) {
return []byte(`
{
"a": "1",
"b": "2"
}
`), nil
},
want: map[string]string{
"a": "1",
"b": "2",
},
wantErr: false,
},
{
name: "The given path is an empty string",
args: args{path: ""},
fileReader: func(name string) ([]byte, error) {
return nil, nil
},
want: map[string]string{},
wantErr: false,
},
{
name: "Load file with broken json",
args: args{path: "file"},
fileReader: func(name string) ([]byte, error) {
return []byte("abc"), nil
},
want: nil,
wantErr: true,
},
{
name: "Load file random error",
args: args{path: "file"},
fileReader: func(name string) ([]byte, error) {
return nil, errors.New("Random Error")
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := LoadMappingFromFile(tt.args.path, tt.fileReader)
if (err != nil) != tt.wantErr {
t.Errorf("LoadMappingFromFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("LoadMappingFromFile() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 7d2450d

Please sign in to comment.