From df3cdca25d9da0d1e6fd31acadbc25eb553f6c3b Mon Sep 17 00:00:00 2001 From: Otavio Fernandes Date: Thu, 3 Nov 2022 17:46:46 +0100 Subject: [PATCH] Build Controller and Inventory --- controllers/inventory_controller.go | 73 +++++++++++ controllers/real_clock.go | 17 +++ pkg/constants/constants.go | 27 ++++ pkg/inventory/fake_test.go | 94 ++++++++++++++ pkg/inventory/interface.go | 13 ++ pkg/inventory/inventory.go | 192 ++++++++++++++++++++++++++++ pkg/inventory/inventory_test.go | 157 +++++++++++++++++++++++ pkg/inventory/sanitize.go | 35 +++++ pkg/inventory/sanitize_test.go | 29 +++++ pkg/inventory/search_result.go | 25 ++++ pkg/util/util.go | 34 +++++ pkg/util/util_test.go | 75 +++++++++++ test/stubs/github_events.go | 47 +++++++ test/stubs/shipwright.go | 74 +++++++++++ 14 files changed, 892 insertions(+) create mode 100644 controllers/inventory_controller.go create mode 100644 controllers/real_clock.go create mode 100644 pkg/constants/constants.go create mode 100644 pkg/inventory/fake_test.go create mode 100644 pkg/inventory/interface.go create mode 100644 pkg/inventory/inventory.go create mode 100644 pkg/inventory/inventory_test.go create mode 100644 pkg/inventory/sanitize.go create mode 100644 pkg/inventory/sanitize_test.go create mode 100644 pkg/inventory/search_result.go create mode 100644 pkg/util/util.go create mode 100644 pkg/util/util_test.go create mode 100644 test/stubs/github_events.go create mode 100644 test/stubs/shipwright.go diff --git a/controllers/inventory_controller.go b/controllers/inventory_controller.go new file mode 100644 index 00000000..1fa96e3b --- /dev/null +++ b/controllers/inventory_controller.go @@ -0,0 +1,73 @@ +package controllers + +import ( + "context" + + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/triggers/pkg/inventory" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// InventoryReconciler reconciles Build instances on the Inventory. +type InventoryReconciler struct { + client.Client // kubernetes client + Scheme *runtime.Scheme // shared scheme + Clock // local clock instance + + buildInventory *inventory.Inventory // local build triggers database +} + +//+kubebuilder:rbac:groups=shipwright.io,resources=builds,verbs=get;list;watch + +// Reconcile reconciles Build instances reflecting it's status on the Inventory. +func (r *InventoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + var b v1alpha1.Build + if err := r.Get(ctx, req.NamespacedName, &b); err != nil { + if !errors.IsNotFound(err) { + logger.Error(err, "Unable to fetch Build, removing from the Inventory") + } + r.buildInventory.Remove(req.NamespacedName) + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if b.ObjectMeta.DeletionTimestamp.IsZero() { + logger.V(0).Info("Adding Build on the Inventory") + r.buildInventory.Add(&b) + } else { + logger.V(0).Info("Removing Build from the Inventory, marked for deletion") + r.buildInventory.Remove(req.NamespacedName) + } + + return ctrl.Result{}, nil +} + +// SetupWithManager uses the manager to watch over Builds. +func (r *InventoryReconciler) SetupWithManager(mgr ctrl.Manager) error { + if r.Clock == nil { + r.Clock = realClock{} + } + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.Build{}). + Complete(r) +} + +// NewInventoryReconciler instantiate the InventoryReconciler. +func NewInventoryReconciler( + ctrlClient client.Client, + scheme *runtime.Scheme, + buildInventory *inventory.Inventory, +) *InventoryReconciler { + return &InventoryReconciler{ + Client: ctrlClient, + Scheme: scheme, + buildInventory: buildInventory, + } +} diff --git a/controllers/real_clock.go b/controllers/real_clock.go new file mode 100644 index 00000000..16d9d10f --- /dev/null +++ b/controllers/real_clock.go @@ -0,0 +1,17 @@ +package controllers + +import ( + "time" +) + +type realClock struct{} + +func (_ realClock) Now() time.Time { + return time.Now() +} + +type Clock interface { + Now() time.Time +} + +// +kubebuilder:docs-gen:collapse=Clock diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 00000000..00287ad6 --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,27 @@ +package constants + +import ( + "fmt" + + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + + tknv1alpha1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1alpha1" + tknv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" +) + +var ( + TektonAPIv1alpha1 = fmt.Sprintf( + "%s/%s", + tknv1alpha1.SchemeGroupVersion.Group, + tknv1alpha1.SchemeGroupVersion.Version, + ) + TektonAPIv1beta1 = fmt.Sprintf("%s/%s", + tknv1beta1.SchemeGroupVersion.Group, + tknv1beta1.SchemeGroupVersion.Version, + ) + ShipwrightAPIVersion = fmt.Sprintf( + "%s/%s", + v1alpha1.SchemeGroupVersion.Group, + v1alpha1.SchemeGroupVersion.Version, + ) +) diff --git a/pkg/inventory/fake_test.go b/pkg/inventory/fake_test.go new file mode 100644 index 00000000..fbf6b8ea --- /dev/null +++ b/pkg/inventory/fake_test.go @@ -0,0 +1,94 @@ +package inventory + +import ( + "log" + "sync" + + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/triggers/test/stubs" + "k8s.io/apimachinery/pkg/types" +) + +// FakeInventory testing instance of Inventory, adds all objects o the local cache, and returns all +// of them on the search queries. +type FakeInventory struct { + m sync.Mutex + + cache map[types.NamespacedName]*v1alpha1.Build +} + +var _ Interface = &FakeInventory{} + +// Contains checks if the informed key is in the cache. +func (i *FakeInventory) Contains(name string) bool { + i.m.Lock() + defer i.m.Unlock() + + log.Printf("Cheking if Build %q is cached", name) + _, ok := i.cache[types.NamespacedName{Namespace: stubs.Namespace, Name: name}] + return ok +} + +// Add adds a Build to the cache. +func (i *FakeInventory) Add(b *v1alpha1.Build) { + i.m.Lock() + defer i.m.Unlock() + + key := types.NamespacedName{Namespace: b.GetNamespace(), Name: b.GetName()} + log.Printf("Adding Build %q to the inventory", key) + i.cache[key] = b +} + +// Remove removes a Build from the cache. +func (i *FakeInventory) Remove(key types.NamespacedName) { + i.m.Lock() + defer i.m.Unlock() + + log.Printf("Removing Build %q from the inventory", key) + delete(i.cache, key) +} + +// search returns all instances as SearchResult slice. +func (i *FakeInventory) search() []SearchResult { + searchResults := []SearchResult{} + if len(i.cache) == 0 { + return searchResults + } + for _, b := range i.cache { + secretName := types.NamespacedName{} + if b.Spec.Trigger != nil && + b.Spec.Trigger.SecretRef != nil && + b.Spec.Trigger.SecretRef.Name != "" { + secretName.Namespace = b.GetNamespace() + secretName.Namespace = b.Spec.Trigger.SecretRef.Name + } + searchResults = append(searchResults, SearchResult{ + BuildName: types.NamespacedName{Namespace: b.GetNamespace(), Name: b.GetName()}, + SecretName: secretName, + }) + } + return searchResults +} + +// SearchForObjectRef returns all Builds in cache. +func (i *FakeInventory) SearchForObjectRef(v1alpha1.TriggerType, *v1alpha1.WhenObjectRef) []SearchResult { + i.m.Lock() + defer i.m.Unlock() + + return i.search() +} + +// SearchForGit returns all Builds in cache. +func (i *FakeInventory) SearchForGit(v1alpha1.TriggerType, string, string) []SearchResult { + i.m.Lock() + defer i.m.Unlock() + + return i.search() +} + +// NewFakeInventory instante a fake inventory for testing. +func NewFakeInventory() *FakeInventory { + return &FakeInventory{ + cache: map[types.NamespacedName]*v1alpha1.Build{}, + } +} diff --git a/pkg/inventory/interface.go b/pkg/inventory/interface.go new file mode 100644 index 00000000..d50811d4 --- /dev/null +++ b/pkg/inventory/interface.go @@ -0,0 +1,13 @@ +package inventory + +import ( + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "k8s.io/apimachinery/pkg/types" +) + +type Interface interface { + Add(*v1alpha1.Build) + Remove(types.NamespacedName) + SearchForObjectRef(v1alpha1.TriggerType, *v1alpha1.WhenObjectRef) []SearchResult + SearchForGit(v1alpha1.TriggerType, string, string) []SearchResult +} diff --git a/pkg/inventory/inventory.go b/pkg/inventory/inventory.go new file mode 100644 index 00000000..27c83980 --- /dev/null +++ b/pkg/inventory/inventory.go @@ -0,0 +1,192 @@ +package inventory + +import ( + "sync" + + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/triggers/pkg/util" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// Inventory keeps track of Build object details, on which it can find objects that match the +// repository URL and trigger rules. +type Inventory struct { + m sync.Mutex + + logger logr.Logger // component logger + cache map[types.NamespacedName]TriggerRules // cache storage +} + +var _ Interface = &Inventory{} + +// TriggerRules keeps the source and webhook trigger information for each Build instance. +type TriggerRules struct { + source v1alpha1.Source + trigger v1alpha1.Trigger +} + +// SearchFn search function signature. +type SearchFn func(TriggerRules) bool + +// Add insert or update an existing record. +func (i *Inventory) Add(b *v1alpha1.Build) { + i.m.Lock() + defer i.m.Unlock() + + if b.Spec.Trigger == nil { + b.Spec.Trigger = &v1alpha1.Trigger{} + } + buildName := types.NamespacedName{Namespace: b.GetNamespace(), Name: b.GetName()} + i.logger.V(0).Info( + "Storing Build on the inventory", + "build-namespace", b.GetNamespace(), + "build-name", b.GetName(), + "generation", b.GetGeneration(), + ) + i.cache[buildName] = TriggerRules{ + source: b.Spec.Source, + trigger: *b.Spec.Trigger, + } +} + +// Remove the informed entry from the cache. +func (i *Inventory) Remove(buildName types.NamespacedName) { + i.m.Lock() + defer i.m.Unlock() + + i.logger.V(0).Info("Removing Build from the inventory", "build-name", buildName) + if _, ok := i.cache[buildName]; !ok { + i.logger.V(0).Info("Inventory entry is not found, skipping deletion!") + return + } + delete(i.cache, buildName) +} + +// loopByWhenType execute the search function informed against each inventory entry, when it returns +// true it returns the build name on the search results instance. +func (i *Inventory) loopByWhenType(triggerType v1alpha1.TriggerType, fn SearchFn) []SearchResult { + found := []SearchResult{} + for k, v := range i.cache { + for _, when := range v.trigger.When { + if triggerType != when.Type { + continue + } + if fn(v) { + secretName := types.NamespacedName{} + if v.trigger.SecretRef != nil { + secretName.Namespace = k.Namespace + secretName.Name = v.trigger.SecretRef.Name + } + found = append(found, SearchResult{ + BuildName: k, + SecretName: secretName, + }) + } + } + } + i.logger.V(0).Info("Build search results", + "amount", len(found), "trigger-type", triggerType) + return found +} + +// SearchForObjectRef search for builds using the ObjectRef as query parameters. +func (i *Inventory) SearchForObjectRef( + triggerType v1alpha1.TriggerType, + objectRef *v1alpha1.WhenObjectRef, +) []SearchResult { + i.m.Lock() + defer i.m.Unlock() + + return i.loopByWhenType(triggerType, func(tr TriggerRules) bool { + for _, w := range tr.trigger.When { + if w.ObjectRef == nil { + continue + } + + // checking the desired status, it must what's informed on the Build object + if len(w.ObjectRef.Status) > 0 && len(objectRef.Status) > 0 { + status := objectRef.Status[0] + if !util.StringSliceContains(w.ObjectRef.Status, status) { + continue + } + } + + // when name is informed it will try to match it first, otherwise the label selector + // matching will take place + if w.ObjectRef.Name != "" { + if objectRef.Name != w.ObjectRef.Name { + continue + } + } else { + if len(w.ObjectRef.Selector) == 0 || len(objectRef.Selector) == 0 { + continue + } + // transforming the matching labels passed to this method as a regular label selector + // instance, which is employed to match against the Build trigger definition + selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{ + MatchLabels: w.ObjectRef.Selector, + }) + if err != nil { + i.logger.V(0).Error(err, "Unable to parse ObjectRef as label-selector", + "ref-selector", w.ObjectRef.Selector) + continue + } + if !selector.Matches(labels.Set(objectRef.Selector)) { + continue + } + } + return true + } + return false + }) +} + +// SearchForGit search for builds using the Git repository details, like the URL, branch name and +// such type of information. +func (i *Inventory) SearchForGit( + triggerType v1alpha1.TriggerType, + repoURL string, + branch string, +) []SearchResult { + i.m.Lock() + defer i.m.Unlock() + + return i.loopByWhenType(triggerType, func(tr TriggerRules) bool { + // first thing to compare, is the repository URL, it must match in order to define the actual + // builds that are representing the repository + if !CompareURLs(repoURL, *tr.source.URL) { + return false + } + + // second part is to search for event-type and compare the informed branch, with the allowed + // branches, configured for that build + for _, w := range tr.trigger.When { + if w.GitHub == nil { + continue + } + for _, b := range w.GitHub.Branches { + if branch == b { + i.logger.V(0).Info("GitHub repository URL matches criteria", + "repo-url", repoURL, "branch", branch) + return true + } + } + } + + return false + }) +} + +// NewInventory instantiate the inventory. +func NewInventory() *Inventory { + logger := logr.New(log.Log.GetSink()) + return &Inventory{ + logger: logger.WithName("component.inventory"), + cache: map[types.NamespacedName]TriggerRules{}, + } +} diff --git a/pkg/inventory/inventory_test.go b/pkg/inventory/inventory_test.go new file mode 100644 index 00000000..0ed509d6 --- /dev/null +++ b/pkg/inventory/inventory_test.go @@ -0,0 +1,157 @@ +package inventory + +import ( + "reflect" + "testing" + + "github.com/onsi/gomega" + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/triggers/test/stubs" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var buildWithTrigger = stubs.ShipwrightBuildWithTriggers( + "ghcr.io/shipwright-io", + "name", + stubs.TriggerWhenPushToMain, +) + +func TestInventory(t *testing.T) { + g := gomega.NewWithT(t) + + i := NewInventory() + + t.Run("adding empty inventory item", func(_ *testing.T) { + i.Add(buildWithTrigger) + g.Expect(len(i.cache)).To(gomega.Equal(1)) + + _, exists := i.cache[types.NamespacedName{Namespace: stubs.Namespace, Name: "name"}] + g.Expect(exists).To(gomega.BeTrue()) + }) + + t.Run("remove inventory item", func(_ *testing.T) { + i.Remove(types.NamespacedName{Namespace: stubs.Namespace, Name: "name"}) + g.Expect(len(i.cache)).To(gomega.Equal(0)) + }) +} + +func TestInventorySearchForgit(t *testing.T) { + g := gomega.NewWithT(t) + + i := NewInventory() + i.Add(buildWithTrigger) + + t.Run("should not find any results", func(_ *testing.T) { + found := i.SearchForGit(v1alpha1.GitHubWebHookTrigger, "", "") + g.Expect(len(found)).To(gomega.Equal(0)) + + found = i.SearchForGit(v1alpha1.GitHubWebHookTrigger, stubs.RepoURL, "") + g.Expect(len(found)).To(gomega.Equal(0)) + }) + + t.Run("should find the build object", func(_ *testing.T) { + found := i.SearchForGit(v1alpha1.GitHubWebHookTrigger, stubs.RepoURL, stubs.Branch) + g.Expect(len(found)).To(gomega.Equal(1)) + }) +} + +func TestInventory_SearchForObjectRef(t *testing.T) { + buildWithObjectRefName := v1alpha1.Build{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: stubs.Namespace, + Name: "buildname", + }, + Spec: v1alpha1.BuildSpec{ + Trigger: &v1alpha1.Trigger{ + When: []v1alpha1.TriggerWhen{{ + Type: v1alpha1.PipelineTrigger, + ObjectRef: &v1alpha1.WhenObjectRef{ + Name: "name", + Status: []string{"Successful"}, + }, + }}, + }, + }, + } + buildWithObjectRefSelector := v1alpha1.Build{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"k": "v"}, + Namespace: stubs.Namespace, + Name: "buildname", + }, + Spec: v1alpha1.BuildSpec{ + Trigger: &v1alpha1.Trigger{ + When: []v1alpha1.TriggerWhen{{ + Type: v1alpha1.PipelineTrigger, + ObjectRef: &v1alpha1.WhenObjectRef{ + Status: []string{"Successful"}, + Selector: map[string]string{"k": "v"}, + }, + }}, + }, + }, + } + + tests := []struct { + name string + builds []v1alpha1.Build + whenType v1alpha1.TriggerType + objectRef v1alpha1.WhenObjectRef + want []SearchResult + }{{ + name: "find build by name", + builds: []v1alpha1.Build{buildWithObjectRefName}, + whenType: v1alpha1.PipelineTrigger, + objectRef: v1alpha1.WhenObjectRef{ + Name: "name", + Status: []string{"Successful"}, + }, + want: []SearchResult{{ + BuildName: types.NamespacedName{Namespace: stubs.Namespace, Name: "buildname"}, + }}, + }, { + name: "find build by label selector", + builds: []v1alpha1.Build{buildWithObjectRefSelector}, + whenType: v1alpha1.PipelineTrigger, + objectRef: v1alpha1.WhenObjectRef{ + Status: []string{"Successful"}, + Selector: map[string]string{"k": "v"}, + }, + want: []SearchResult{{ + BuildName: types.NamespacedName{Namespace: stubs.Namespace, Name: "buildname"}, + }}, + }, { + name: "does not find builds, due to wrong selector", + builds: []v1alpha1.Build{buildWithObjectRefSelector}, + whenType: v1alpha1.PipelineTrigger, + objectRef: v1alpha1.WhenObjectRef{ + Status: []string{"Successful"}, + Selector: map[string]string{"wrong": "label"}, + }, + want: []SearchResult{}, + }, { + name: "does not find builds, due to wrong name", + builds: []v1alpha1.Build{buildWithObjectRefSelector}, + whenType: v1alpha1.PipelineTrigger, + objectRef: v1alpha1.WhenObjectRef{ + Name: "wrong", + Status: []string{"Successful"}, + }, + want: []SearchResult{}, + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := NewInventory() + for _, b := range tt.builds { + i.Add(&b) + } + + got := i.SearchForObjectRef(tt.whenType, &tt.objectRef) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Inventory.SearchForObjectRef() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/inventory/sanitize.go b/pkg/inventory/sanitize.go new file mode 100644 index 00000000..a310e654 --- /dev/null +++ b/pkg/inventory/sanitize.go @@ -0,0 +1,35 @@ +package inventory + +import ( + "fmt" + "net/url" + "strings" +) + +// SanitizeURL takes a raw repository URL and returns only the hostname and path, removing possible +// prefix protocol, and extension suffixes. +func SanitizeURL(rawURL string) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return "", err + } + + urlPath := strings.TrimSuffix(u.EscapedPath(), ".git") + return fmt.Sprintf("%s%s", u.Hostname(), urlPath), nil +} + +// CompareURLs compare the informed URLs. +func CompareURLs(a, b string) bool { + if a == b { + return true + } + aSanitized, err := SanitizeURL(a) + if err != nil { + return false + } + bSanitized, err := SanitizeURL(b) + if err != nil { + return false + } + return aSanitized == bSanitized +} diff --git a/pkg/inventory/sanitize_test.go b/pkg/inventory/sanitize_test.go new file mode 100644 index 00000000..df1bc45b --- /dev/null +++ b/pkg/inventory/sanitize_test.go @@ -0,0 +1,29 @@ +package inventory + +import "testing" + +func TestSanitizeURL(t *testing.T) { + tests := []struct { + name string + rawURL string + want string + wantErr bool + }{{ + "complete URL, with prefix and suffix", + "https://github.com/username/repository.git", + "github.com/username/repository", + false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := SanitizeURL(tt.rawURL) + if (err != nil) != tt.wantErr { + t.Errorf("SanitizeURL() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SanitizeURL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/inventory/search_result.go b/pkg/inventory/search_result.go new file mode 100644 index 00000000..4743e612 --- /dev/null +++ b/pkg/inventory/search_result.go @@ -0,0 +1,25 @@ +package inventory + +import ( + "k8s.io/apimachinery/pkg/types" +) + +// SearchResult contains a Inventory result item. +type SearchResult struct { + BuildName types.NamespacedName // build name maching criteria + SecretName types.NamespacedName // respective secret coordinates (for webhook) +} + +// HasSecret assert if the SecretName is defined. +func (s *SearchResult) HasSecret() bool { + return s.SecretName.Namespace != "" && s.SecretName.Name != "" +} + +// ExtractBuildNames picks the build names from informed SearchResult slice. +func ExtractBuildNames(results ...SearchResult) []string { + var names []string + for _, entry := range results { + names = append(names, entry.BuildName.Name) + } + return names +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 00000000..6bb1f113 --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,34 @@ +package util + +import ( + "fmt" +) + +// StringSliceContains assert if the informed slice contains a string. +func StringSliceContains(slice []string, str string) bool { + for _, s := range slice { + if str == s { + return true + } + } + return false +} + +// JoinReversedStringSliceForK8s joins the entries of the informed slice reversed using commas in a +// single string limited to 63 characters, a Kubernetes limitation for labels. +func JoinReversedStringSliceForK8s(slice []string) string { + var s string + for i := len(slice) - 1; i >= 0; i-- { + entry := slice[i] + if len(s)+len(entry) >= 63 { + break + } + + if len(s) == 0 { + s = entry + } else { + s = fmt.Sprintf("%s,%s", s, entry) + } + } + return s +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 00000000..a93ea08c --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,75 @@ +package util + +import ( + "strconv" + "testing" +) + +func TestStringSliceContains(t *testing.T) { + tests := []struct { + name string + slice []string + str string + want bool + }{{ + name: "empty slice and empty string", + slice: []string{}, + str: "", + want: false, + }, { + name: "empty slice", + slice: []string{}, + str: "want", + want: false, + }, { + name: "regular slice and string", + slice: []string{"a", "b"}, + str: "a", + want: true, + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := StringSliceContains(tt.slice, tt.str); got != tt.want { + t.Errorf("StringSliceContains() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestJoinReversedStringSliceForK8s(t *testing.T) { + var longSlice []string + for i := 0; i <= 100; i++ { + longSlice = append(longSlice, strconv.Itoa(i)) + } + + tests := []struct { + name string + slice []string + want string + }{{ + name: "single entry", + slice: []string{"a"}, + want: "a", + }, { + name: "two entries", + slice: []string{"b", "a"}, + want: "a,b", + }, { + name: "way too long slice, should be capped at 63 characters", + slice: longSlice, + want: "100,99,98,97,96,95,94,93,92,91,90,89,88,87,86,85,84,83,82,81,80", + }} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if len(tt.want) > 63 { + t.Errorf("want is loo long (> 63): %v", tt.want) + } + if got := JoinReversedStringSliceForK8s(tt.slice); got != tt.want { + t.Errorf("JoinReversedStringSliceForK8s() = %v, want %v, len(slice) %v", + got, tt.want, len(tt.slice)) + } + }) + } +} diff --git a/test/stubs/github_events.go b/test/stubs/github_events.go new file mode 100644 index 00000000..6e533290 --- /dev/null +++ b/test/stubs/github_events.go @@ -0,0 +1,47 @@ +package stubs + +import ( + "time" + + "github.com/google/go-github/v42/github" +) + +var RepoURL = "https://github.com/shipwright-io/sample-nodejs" + +const ( + RepoFullName = "shipwright-io/sample-nodejs" + HeadCommitID = "commit-id" + HeadCommitMsg = "commit message" + HeadCommitAuthorName = "Author's Name" + BeforeCommitID = "before-commit-id" + GitRef = "refs/heads/main" +) + +func GitHubPingEvent() github.PingEvent { + return github.PingEvent{ + Zen: github.String("zen"), + HookID: github.Int64(0), + Installation: &github.Installation{}, + } +} + +func GitHubPushEvent() github.PushEvent { + return github.PushEvent{ + Repo: &github.PushEventRepository{ + HTMLURL: github.String(RepoURL), + FullName: github.String(RepoFullName), + }, + HeadCommit: &github.HeadCommit{ + ID: github.String(HeadCommitID), + Message: github.String(HeadCommitMsg), + Timestamp: &github.Timestamp{ + Time: time.Date(2022, time.March, 1, 0, 0, 0, 0, time.Local), + }, + Author: &github.CommitAuthor{ + Name: github.String(HeadCommitAuthorName), + }, + }, + Before: github.String(BeforeCommitID), + Ref: github.String(GitRef), + } +} diff --git a/test/stubs/shipwright.go b/test/stubs/shipwright.go new file mode 100644 index 00000000..29aec23d --- /dev/null +++ b/test/stubs/shipwright.go @@ -0,0 +1,74 @@ +package stubs + +import ( + "fmt" + + "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + Namespace = "default" + Branch = "main" + PipelineNameInTrigger = "pipeline" +) + +var ( + // TriggerWhenPushToMain describes a trigger for a github push event on default branch. + TriggerWhenPushToMain = v1alpha1.TriggerWhen{ + Type: v1alpha1.GitHubWebHookTrigger, + GitHub: &v1alpha1.WhenGitHub{ + Events: []v1alpha1.GitHubEventName{ + v1alpha1.GitHubPushEvent, + }, + Branches: []string{Branch}, + }, + } + // TriggerWhenPipelineSucceeded describes a trigger for Tekton Pipeline on status "succeeded". + TriggerWhenPipelineSucceeded = v1alpha1.TriggerWhen{ + Type: v1alpha1.PipelineTrigger, + ObjectRef: &v1alpha1.WhenObjectRef{ + Name: PipelineNameInTrigger, + Status: []string{"Succeeded"}, + Selector: map[string]string{}, + }, + } +) + +// ShipwrightBuild returns a Build using informed output image base and name. +func ShipwrightBuild(outputImageBase, name string) *v1alpha1.Build { + strategyKind := v1alpha1.BuildStrategyKind("ClusterBuildStrategy") + contextDir := "source-build" + + return &v1alpha1.Build{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: Namespace, + Name: name, + }, + Spec: v1alpha1.BuildSpec{ + Strategy: v1alpha1.Strategy{ + Kind: &strategyKind, + Name: "buildpacks-v3", + }, + Source: v1alpha1.Source{ + URL: &RepoURL, + ContextDir: &contextDir, + }, + Output: v1alpha1.Image{ + Image: fmt.Sprintf("%s/%s:latest", outputImageBase, name), + }, + }, + } +} + +// ShipwrightBuildWithTriggers creates a Build with optional triggers. +func ShipwrightBuildWithTriggers( + outputImageBase, + name string, + triggers ...v1alpha1.TriggerWhen, +) *v1alpha1.Build { + b := ShipwrightBuild(outputImageBase, name) + b.Spec.Trigger = &v1alpha1.Trigger{When: triggers} + return b +}