diff --git a/api/v1alpha1/pattern_types.go b/api/v1alpha1/pattern_types.go index aac9d5b65..ac36d0f46 100644 --- a/api/v1alpha1/pattern_types.go +++ b/api/v1alpha1/pattern_types.go @@ -109,11 +109,6 @@ type GitConfig struct { // +operator-sdk:csv:customresourcedefinitions:type=spec,order=15,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:hidden"} OriginRevision string `json:"originRevision,omitempty"` - // Interval in seconds to poll for drifts between origin and target repositories. Default: 180 seconds - // +operator-sdk:csv:customresourcedefinitions:type=spec,order=16,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:number","urn:alm:descriptor:com.tectonic.ui:advanced"} - // +kubebuilder:default:=180 - PollInterval int `json:"pollInterval,omitempty"` - // Optional. FQDN of the git server if automatic parsing from TargetRepo is broken // +operator-sdk:csv:customresourcedefinitions:type=spec,order=17,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:advanced"} Hostname string `json:"hostname,omitempty"` diff --git a/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml b/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml index 5df01ac4e..2af229e86 100644 --- a/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml +++ b/config/crd/bases/gitops.hybrid-cloud-patterns.io_patterns.yaml @@ -107,11 +107,6 @@ spec: description: (DEPRECATED) Branch, tag or commit in the upstream git repository. Does not support short-sha's. Default to HEAD type: string - pollInterval: - default: 180 - description: 'Interval in seconds to poll for drifts between origin - and target repositories. Default: 180 seconds' - type: integer targetRepo: description: Git repo containing the pattern to deploy. Must use https/http or, for ssh, git@server:foo/bar.git diff --git a/internal/controller/drift.go b/internal/controller/drift.go deleted file mode 100644 index 031310454..000000000 --- a/internal/controller/drift.go +++ /dev/null @@ -1,393 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - "sort" - "sync" - "time" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-logr/logr" - api "github.com/hybrid-cloud-patterns/patterns-operator/api/v1alpha1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -//go:generate mockgen -source $GOFILE -package=$GOPACKAGE -destination=mock_$GOFILE - -var ( - conditionMsgs = map[api.PatternConditionType]string{ - api.GitOutOfSync: "Git repositories are out of sync", - api.GitInSync: "Git repositories are in sync"} -) - -type repositoryPair struct { - gitClient GitClient - kClient client.Client - name, namespace string - interval time.Duration - lastCheck, nextCheck time.Time -} - -func (r *repositoryPair) hasDrifted() (bool, error) { - p := &api.Pattern{} - err := r.kClient.Get(context.Background(), types.NamespacedName{Name: r.name, Namespace: r.namespace}, p) - if err != nil { - return false, err - } - if p.Spec.GitConfig.OriginRepo == "" || p.Spec.GitConfig.TargetRepo == "" { - return false, fmt.Errorf("git config does not contain origin and targer repositories") - } - origin := r.gitClient.NewRemoteClient(&config.RemoteConfig{Name: "origin", URLs: []string{p.Spec.GitConfig.OriginRepo}}) - target := r.gitClient.NewRemoteClient(&config.RemoteConfig{Name: "target", URLs: []string{p.Spec.GitConfig.TargetRepo}}) - - originRefs, err := origin.List(&git.ListOptions{}) - if err != nil { - return false, err - } - if len(originRefs) == 0 { - return false, fmt.Errorf("no references found for origin %s", p.Spec.GitConfig.OriginRepo) - } - targetRefs, err := target.List(&git.ListOptions{}) - if err != nil { - return false, err - } - if len(targetRefs) == 0 { - return false, fmt.Errorf("no references found for target %s", p.Spec.GitConfig.TargetRepo) - } - var originRef *plumbing.Reference - originRefName := plumbing.HEAD - if p.Spec.GitConfig.OriginRevision != "" { - originRefName = plumbing.NewBranchReferenceName(p.Spec.GitConfig.OriginRevision) - originRef = getReferenceByName(originRefs, originRefName) - } else { - originRef = getHeadBranch(originRefs) - } - if originRef == nil { - return false, fmt.Errorf("unable to find %s for origin %s", originRefName, p.Spec.GitConfig.OriginRepo) - } - - var targetRef *plumbing.Reference - targetRefName := plumbing.HEAD - if p.Spec.GitConfig.TargetRevision != "" { - targetRefName = plumbing.NewBranchReferenceName(p.Spec.GitConfig.TargetRevision) - targetRef = getReferenceByName(targetRefs, targetRefName) - } else { - targetRef = getHeadBranch(targetRefs) - } - if targetRef == nil { - return false, fmt.Errorf("unable to find %s for target %s", targetRefName, p.Spec.GitConfig.TargetRepo) - } - return originRef.Hash() != targetRef.Hash(), nil -} - -type repositoryPairs []*repositoryPair - -func (r repositoryPairs) Len() int { - return len(r) -} - -func (r repositoryPairs) Less(i, j int) bool { - return r[i].nextCheck.Before(r[j].nextCheck) -} - -func (r repositoryPairs) Swap(i, j int) { - r[i], r[j] = r[j], r[i] -} - -type RemoteClient interface { - List(o *git.ListOptions) (rfs []*plumbing.Reference, err error) -} - -type GitClient interface { - NewRemoteClient(c *config.RemoteConfig) RemoteClient -} - -type gitClient struct { -} - -func newGitClient() GitClient { - return &gitClient{} -} - -func (c *gitClient) NewRemoteClient(conf *config.RemoteConfig) RemoteClient { - return git.NewRemote(nil, conf) -} - -type watcher struct { - kClient client.Client - // endCh is used to notify the watch routine to exit the loop - endCh, updateCh chan any - repoPairs repositoryPairs - mutex *sync.Mutex - logger logr.Logger - timer *time.Timer - timerCancelled bool - gitClient GitClient -} - -//nolint:gocritic -func newDriftWatcher(kubeClient client.Client, logger logr.Logger, gitClient GitClient) (driftWatcher, chan any) { - d := &watcher{ - kClient: kubeClient, - logger: logger, - repoPairs: repositoryPairs{}, - endCh: make(chan any), - mutex: &sync.Mutex{}, - gitClient: gitClient} - return d, d.watch() -} - -type driftWatcher interface { - add(name, namespace string, interval int) error - updateInterval(name, namespace string, interval int) error - remove(name, namespace string) error - watch() chan any - isWatching(name, namespace string) bool -} - -// isWatching returns true if the pair name,namespace reference is being monitored for drifts, false otherwise -func (d *watcher) isWatching(name, namespace string) bool { - d.mutex.Lock() - defer d.mutex.Unlock() - for _, item := range d.repoPairs { - if item.name == name && item.namespace == namespace { - return true - } - } - return false -} - -// add instructs the client to start monitoring for drifts between two repositories -func (d *watcher) add(name, namespace string, interval int) error { - if d.updateCh == nil { - return fmt.Errorf("unable to add %s in %s when watch has not yet started", name, namespace) - } - d.mutex.Lock() - defer d.mutex.Unlock() - d.stopTimer() - pair := repositoryPair{ - name: name, - namespace: namespace, - kClient: d.kClient, - interval: time.Duration(interval) * time.Second, - nextCheck: time.Now().Add(time.Duration(interval) * time.Second), - gitClient: d.gitClient} - d.repoPairs = append(d.repoPairs, &pair) - sort.Sort(d.repoPairs) - // Notify of updates - d.updateCh <- struct{}{} - return nil -} - -// update checks if the new interval differs from the stored one and requeues the reference to ensure the new interval is reflected -func (d *watcher) updateInterval(name, namespace string, interval int) error { - if d.updateCh == nil { - return fmt.Errorf("unable to update interval for %s in %s when watch has not yet started", name, namespace) - } - d.mutex.Lock() - defer d.mutex.Unlock() - for index, item := range d.repoPairs { - if item.name == name && item.namespace == namespace { - if item.interval != time.Duration(interval)*time.Second { - d.stopTimer() - d.logger.V(1).Info(fmt.Sprintf("New interval detected for %s in %s: %d second(s)", name, namespace, interval)) - pair := repositoryPair{ - name: name, - namespace: namespace, - kClient: d.kClient, - interval: time.Duration(interval) * time.Second, - nextCheck: time.Now().Add(time.Duration(interval) * time.Second), - gitClient: d.gitClient} - d.repoPairs = append(d.repoPairs[:index], d.repoPairs[index+1:]...) - d.repoPairs = append(d.repoPairs, &pair) - sort.Sort(d.repoPairs) - // Notify of updates - d.updateCh <- struct{}{} - return nil - } - } - } - return nil -} - -// remove instructs the client to stop monitoring for drifts for the given resource name and namespace -// -//nolint:gocritic -func (d *watcher) remove(name, namespace string) error { - if d.updateCh == nil { - return fmt.Errorf("unable to remove %s in %s when watch has not yet started", name, namespace) - } - d.mutex.Lock() - defer d.mutex.Unlock() - for index := range d.repoPairs { - if name == d.repoPairs[index].name && namespace == d.repoPairs[index].namespace { - d.stopTimer() - d.repoPairs = append(d.repoPairs[:index], d.repoPairs[index+1:]...) - sort.Sort(d.repoPairs) - // Notify of updates - d.updateCh <- struct{}{} - return nil - } - } - return fmt.Errorf("unable to find git remote pair for pattern %s in namespace %s", name, namespace) -} - -func (d *watcher) stopTimer() { - // if there is an ongoing timer... - if d.timer != nil { - // ...stop the timer. Any ongoing timer is no longer valid as there are going to be changes to the slice - if d.timer.Stop() { - // if the timer function is in progress, at this point is waiting - // to get the lock. Flag timerCancelled as true to notify the - // routine to exit as soon as it gets the lock and ensure that the - // function does not continue executing, as the order of the slice - // has changed since the function was triggered and got blocked - // waiting to get the lock - d.timerCancelled = true - } - } -} - -func (d *watcher) startNewTimer() { - d.mutex.Lock() - defer d.mutex.Unlock() - if len(d.repoPairs) == 0 { - return - } - // slice is already sorted from a previous call to Add or Remove or from a previous timer - nextPair := d.repoPairs[0] - nextInterval := time.Until(nextPair.nextCheck) - if time.Now().After(nextPair.nextCheck) { - // In case there is an overdue check, which would result in a negative value, we set it to 0 so that it is triggered right away - d.logger.V(1).Info(fmt.Sprintf("Next interval is negative, resetting to 0 %s: %s - %s\n", nextInterval.String(), time.Now().String(), nextPair.nextCheck.String())) - nextInterval = 0 - } - // start a timer and execute drift check when timer expires - d.timer = time.AfterFunc(nextInterval, func() { - d.mutex.Lock() - defer d.mutex.Unlock() - if d.timerCancelled { - // timer has been stopped while the routine was waiting for hold - // the lock. This means that there has been a change in the order - // of elements in the slice while it was waiting to obtain the lock - // reset the timer canceled field. - d.timerCancelled = false - return - } - if len(d.repoPairs) == 0 { - d.updateCh <- struct{}{} - return - } - pair := d.repoPairs[0] - hasDrifted, err := pair.hasDrifted() - if err != nil { - d.logger.Error(err, "found error while detecting drift") - } else { - conditionType := api.GitInSync - if hasDrifted { - d.logger.Info(fmt.Sprintf("git repositories have drifted for resource %s in namespace %s", pair.name, pair.namespace)) - conditionType = api.GitOutOfSync - } - err := updatePatternConditions(d.kClient, conditionType, pair.name, pair.namespace, time.Now()) - if err != nil { - d.logger.Error(err, fmt.Sprintf("failed to update pattern condition for %s in namespace %s", pair.name, pair.namespace)) - } - } - pair.lastCheck = time.Now() - pair.nextCheck = pair.lastCheck.Add(pair.interval) - d.repoPairs[0] = pair - // recalculate next timer - sort.Sort(d.repoPairs) - d.updateCh <- struct{}{} - }) - d.logger.V(1).Info(fmt.Sprintf("New timer started for %s in %s to end on %s", nextPair.name, nextPair.namespace, nextPair.nextCheck.String())) -} - -// watch starts the process of monitoring the drifts. The call returns a channel to be used to manage -// the closure of the monitoring routine cleanly. -func (d *watcher) watch() chan any { - if d.updateCh != nil { - return d.endCh - } - // ready to start processing notifications - d.updateCh = make(chan any) - go func() { - for { - select { - case <-d.endCh: - if d.timer != nil { - d.timer.Stop() - } - return - case <-d.updateCh: - go d.startNewTimer() - } - } - }() - d.updateCh <- struct{}{} - return d.endCh -} - -func updatePatternConditions(kcli client.Client, conditionType api.PatternConditionType, name, namespace string, timestamp time.Time) error { - var pattern api.Pattern - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // fetch the pattern object - err := kcli.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, &pattern) - if err != nil { - return err - } - // get the current active condition - currentIndex, currentCondition := getPatternConditionByStatus(pattern.Status.Conditions, v1.ConditionTrue) - if currentCondition != nil && currentCondition.Type != conditionType { - // mark the current condition with status false and update timestamp - currentCondition.Status = v1.ConditionFalse - currentCondition.LastUpdateTime = metav1.Time{Time: timestamp} - pattern.Status.Conditions[currentIndex] = *currentCondition - } - // get the condition by status - index, condition := getPatternConditionByType(pattern.Status.Conditions, conditionType) - if condition == nil { - // condition not yet found, so we create a new one - condition = &api.PatternCondition{ - Type: conditionType, - Status: v1.ConditionTrue, - LastUpdateTime: metav1.Time{Time: timestamp}, - LastTransitionTime: metav1.Time{Time: timestamp}, - Message: conditionMsgs[conditionType]} - pattern.Status.Conditions = append(pattern.Status.Conditions, *condition) - return kcli.Status().Update(ctx, &pattern) - } - condition.LastUpdateTime = metav1.Time{Time: timestamp} - if condition.Status == v1.ConditionTrue { - pattern.Status.Conditions[index] = *condition - return kcli.Status().Update(ctx, &pattern) - } - // Not current condition, so we make it so - condition.Status = v1.ConditionTrue - condition.LastTransitionTime = metav1.Time{Time: timestamp} - pattern.Status.Conditions[index] = *condition - return kcli.Status().Update(ctx, &pattern) -} - -func getHeadBranch(refs []*plumbing.Reference) *plumbing.Reference { - headRef := getReferenceByName(refs, plumbing.HEAD) - if headRef == nil { - return nil - } - return getReferenceByName(refs, headRef.Target()) -} -func getReferenceByName(refs []*plumbing.Reference, referenceName plumbing.ReferenceName) *plumbing.Reference { - for _, ref := range refs { - if ref.Name() == referenceName { - return ref - } - } - return nil -} diff --git a/internal/controller/drift_test.go b/internal/controller/drift_test.go deleted file mode 100644 index 9defd8928..000000000 --- a/internal/controller/drift_test.go +++ /dev/null @@ -1,656 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - "math/rand" - "sort" - "sync" - "time" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-logr/logr" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - gomock "go.uber.org/mock/gomock" - v1core "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/log" - - api "github.com/hybrid-cloud-patterns/patterns-operator/api/v1alpha1" -) - -const ( - mainReference plumbing.ReferenceName = "refs/heads/main" - originURL string = "https://origin.url" - targetURL string = "https://target.url" - hashCommitMainHead string = "667679cce3942d3dec754b29d0f97500bba57978" - hashCommitAmendedHead string = "6ffb7b8f89075d66fba48c4d0000f8fb52720cf1" - hashCommitTestBranch string = "0e34ab1c94a4b588ddea45087e956b22bddfa8a2" - hashCommitBugfixBranch string = "597db674d31dee964f464d84ee0b4f3797bb06dd" - foo string = "foo" - bar string = "bar" - defaultNamespace string = "default" -) - -var ( - firstCommitReference = []*plumbing.Reference{ - plumbing.NewSymbolicReference(plumbing.HEAD, mainReference), - plumbing.NewHashReference(mainReference, plumbing.NewHash(hashCommitMainHead))} - firstCommitAmendedReference = []*plumbing.Reference{ - plumbing.NewSymbolicReference(plumbing.HEAD, mainReference), - plumbing.NewHashReference(mainReference, - plumbing.NewHash(hashCommitAmendedHead))} - firstCommitReferenceWithMaster = []*plumbing.Reference{ - plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.Master), - plumbing.NewHashReference(plumbing.Master, - plumbing.NewHash(hashCommitMainHead))} - multipleCommitsReference = []*plumbing.Reference{ - plumbing.NewSymbolicReference(plumbing.HEAD, mainReference), - plumbing.NewHashReference(mainReference, - plumbing.NewHash(hashCommitMainHead)), - plumbing.NewHashReference(plumbing.NewBranchReferenceName("test"), - plumbing.NewHash(hashCommitMainHead)), - plumbing.NewHashReference(plumbing.NewBranchReferenceName("bugfix"), - plumbing.NewHash(hashCommitMainHead)), - } - multipleCommitsWithDifferentHashReference = []*plumbing.Reference{ - plumbing.NewSymbolicReference(plumbing.HEAD, mainReference), - plumbing.NewHashReference(mainReference, - plumbing.NewHash(hashCommitAmendedHead)), - plumbing.NewHashReference(plumbing.NewBranchReferenceName("bugfix"), - plumbing.NewHash(hashCommitBugfixBranch)), - plumbing.NewHashReference(plumbing.NewBranchReferenceName("test"), - plumbing.NewHash(hashCommitTestBranch)), - } - noHeadReference = []*plumbing.Reference{ - plumbing.NewHashReference(mainReference, - plumbing.NewHash("first Commit"))} - - noCommits = []*plumbing.Reference{plumbing.NewSymbolicReference(plumbing.HEAD, mainReference)} - emptyCommits = []*plumbing.Reference{} -) -var _ = Describe("Git client", func() { - - var _ = Context("when interacting with Git", func() { - var ( - mockGitClient *MockGitClient - mockRemoteClientOrigin, mockRemoteClientTarget *MockRemoteClient - pattern api.Pattern - ) - - BeforeEach(func() { - ctrl := gomock.NewController(GinkgoT()) - mockGitClient = NewMockGitClient(ctrl) - mockRemoteClientOrigin = NewMockRemoteClient(ctrl) - mockRemoteClientTarget = NewMockRemoteClient(ctrl) - }) - - AfterEach(func() { - e := k8sClient.Delete(context.Background(), &pattern) - Expect(e).NotTo(HaveOccurred()) - }) - DescribeTable("when drifting", func(originRefs, targetRefs []*plumbing.Reference, originRef, targetRef string, expected bool, - errOriginList, errTargetList, errOriginFilter, errTargetFilter error) { - pattern = api.Pattern{ - ObjectMeta: v1.ObjectMeta{Name: foo, Namespace: defaultNamespace}, - TypeMeta: v1.TypeMeta{Kind: "Pattern", APIVersion: api.GroupVersion.String()}, - Spec: api.PatternSpec{ - GitConfig: api.GitConfig{ - Hostname: foo, - PollInterval: 30, - OriginRepo: originURL, - OriginRevision: originRef, - TargetRepo: targetURL, - TargetRevision: targetRef}}, - } - e := k8sClient.Create(context.Background(), &pattern) - Expect(e).NotTo(HaveOccurred()) - - remote := repositoryPair{ - name: foo, - namespace: defaultNamespace, - gitClient: mockGitClient, - kClient: k8sClient, - } - mockGitClient.EXPECT().NewRemoteClient(&config.RemoteConfig{Name: "origin", URLs: []string{originURL}}).Times(1).Return(mockRemoteClientOrigin) - mockGitClient.EXPECT().NewRemoteClient(&config.RemoteConfig{Name: "target", URLs: []string{targetURL}}).Times(1).Return(mockRemoteClientTarget) - mockRemoteClientOrigin.EXPECT().List(&git.ListOptions{}).Times(1).Return(originRefs, errOriginList) - if errOriginList == nil { - mockRemoteClientTarget.EXPECT().List(&git.ListOptions{}).Times(1).Return(targetRefs, errTargetList) - } - - hasDrifted, e := remote.hasDrifted() - if e != nil { - switch { - case errOriginList != nil: - Expect(e).To(Equal(errOriginList)) - case errTargetList != nil: - Expect(e).To(Equal(errTargetList)) - case errOriginFilter != nil: - Expect(e).To(Equal(errOriginFilter)) - case errTargetFilter != nil: - Expect(e).To(Equal(errTargetFilter)) - } - return - } - - Expect(e).NotTo(HaveOccurred()) - Expect(hasDrifted).To(Equal(expected)) - }, - Entry("One commit with head main and same hash", firstCommitReference, firstCommitReference, - "", "", false, nil, nil, nil, nil), - Entry("One commit with head main and different hash", firstCommitReference, firstCommitAmendedReference, - "", "", true, nil, nil, nil, nil), - Entry("One commit with head main and head master and same hash", firstCommitReference, firstCommitReferenceWithMaster, - "", "", false, nil, nil, nil, nil), - Entry("Multiple commit with head main and branches with the same hash", multipleCommitsReference, multipleCommitsReference, - "", "", false, nil, nil, nil, nil), - Entry("Multiple commit with head main and branches with different hash", multipleCommitsReference, multipleCommitsWithDifferentHashReference, - "", "", true, nil, nil, nil, nil), - Entry("One commit with head main and target reference with the same hash", firstCommitReference, multipleCommitsReference, - "", "test", false, nil, nil, nil, nil), - Entry("One commit with origin reference and target reference with the same hash", firstCommitReference, multipleCommitsReference, - "test", "test", false, nil, nil, nil, nil), - // errors - Entry("Error while retrieving the origin references", emptyCommits, nil, - "", "", false, fmt.Errorf("no references found for origin %s", originURL), nil, nil, nil), - Entry("Error while retrieving the target references", firstCommitReference, nil, - "", "", false, nil, fmt.Errorf("error while retrieving target references %s", targetURL), nil, nil), - Entry("One commit with no HEAD reference in origin", noHeadReference, noHeadReference, - "", "", false, nil, nil, fmt.Errorf("unable to find HEAD for origin %s", originURL), nil), - Entry("One commit with no HEAD reference in target", firstCommitReference, noHeadReference, - "", "", false, nil, nil, nil, fmt.Errorf("unable to find HEAD for target %s", targetURL)), - Entry("No commits found in origin", noCommits, noHeadReference, - "", "", false, nil, nil, fmt.Errorf("unable to find HEAD for origin %s", originURL), nil), - Entry("No commits found in target", firstCommitReference, noCommits, - "", "", false, nil, nil, nil, fmt.Errorf("unable to find HEAD for target %s", targetURL)), - Entry("Reference not found in origin", firstCommitAmendedReference, firstCommitReference, - "reference/not/found", "", false, nil, nil, fmt.Errorf("unable to find refs/heads/reference/not/found for origin %s", originURL), nil), - Entry("Reference not found in target", firstCommitAmendedReference, firstCommitReference, - "", "reference/not/found", false, nil, nil, nil, fmt.Errorf("unable to find refs/heads/reference/not/found for target %s", targetURL)), - ) - }) - var _ = Context("git reference", func() { - - DescribeTable("when retrieving the git reference", func(references []*plumbing.Reference, targetRef plumbing.ReferenceName, expected *plumbing.Reference) { - ret := getReferenceByName(references, targetRef) - if expected == nil { - Expect(ret).To(BeNil()) - return - } - Expect(expected).To(Equal(ret)) - }, - Entry("When filtering for HEAD symbolic link and is found", firstCommitReference, plumbing.HEAD, - plumbing.NewSymbolicReference(plumbing.HEAD, mainReference)), - Entry("When filtering for ref/heads/main and is found", firstCommitReference, - mainReference, plumbing.NewHashReference(mainReference, plumbing.NewHash(hashCommitMainHead))), - - // errors - Entry("When the symbolic link for HEAD is not found", noHeadReference, plumbing.HEAD, nil), - Entry("When the reference is not found", noCommits, mainReference, nil), - ) - - }) - var _ = Context("When interacting with the pair slice", func() { - - var ( - one = &repositoryPair{name: "one", namespace: "default", nextCheck: time.Time{}.Add(time.Second)} - three = &repositoryPair{name: "three", namespace: "default", nextCheck: time.Time{}.Add(3 * time.Second)} - four = &repositoryPair{name: "four", namespace: "default", nextCheck: time.Time{}.Add(4 * time.Second)} - five = &repositoryPair{name: "second", namespace: "default", nextCheck: time.Time{}.Add(5 * time.Second)} - ) - It("sorts correctly the order", func() { - watch := newWatcher(nil) - watch.watch() - By("adding four elements") - watch.repoPairs = []*repositoryPair{five, three, one, four} - sort.Sort(watch.repoPairs) - Expect(watch.repoPairs).To(HaveLen(4)) - Expect(watch.repoPairs[0]).To(BeEquivalentTo(one)) - Expect(watch.repoPairs[1]).To(BeEquivalentTo(three)) - Expect(watch.repoPairs[2]).To(BeEquivalentTo(four)) - Expect(watch.repoPairs[3]).To(BeEquivalentTo(five)) - By("removing the first element") - watch.repoPairs = watch.repoPairs[1:] - sort.Sort(watch.repoPairs) - Expect(watch.repoPairs[0]).To(BeEquivalentTo(three)) - Expect(watch.repoPairs[1]).To(BeEquivalentTo(four)) - Expect(watch.repoPairs[2]).To(BeEquivalentTo(five)) - }) - - }) - - var _ = Context("When updating the pattern conditions", func() { - - var ( - ctx = context.Background() - pattern api.Pattern - ) - - BeforeEach(func() { - pattern = api.Pattern{ - ObjectMeta: v1.ObjectMeta{Name: foo, Namespace: defaultNamespace}, - TypeMeta: v1.TypeMeta{Kind: "Pattern", APIVersion: api.GroupVersion.String()}, - Spec: api.PatternSpec{GitConfig: api.GitConfig{Hostname: foo, PollInterval: 30}}, - } - e := k8sClient.Create(ctx, &pattern) - Expect(e).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - e := k8sClient.Delete(ctx, &pattern) - Expect(e).NotTo(HaveOccurred()) - }) - It("adds the first condition", func() { - var p api.Pattern - timestamp := time.Time{}.Add(1 * time.Second) - By("validating the pattern has no conditions yet") - err := k8sClient.Get(ctx, types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &p) - Expect(err).NotTo(HaveOccurred()) - Expect(p).NotTo(BeNil()) - Expect(p.Status.Conditions).To(BeEmpty()) - By("calling the update pattern conditions to add a new condition") - e := updatePatternConditions(k8sClient, api.GitInSync, foo, defaultNamespace, timestamp) - Expect(e).NotTo(HaveOccurred()) - By("retrieving the pattern object once more and validating that it contains the new condition") - err = k8sClient.Get(ctx, types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &p) - Expect(err).NotTo(HaveOccurred()) - Expect(p).NotTo(BeNil()) - Expect(p.Status.Conditions).To(HaveLen(1)) - Expect(p.Status.Conditions[0]).To(BeComparableTo(api.PatternCondition{ - Type: api.GitInSync, - Status: v1core.ConditionTrue, - LastUpdateTime: v1.Time{Time: timestamp}, - LastTransitionTime: v1.Time{Time: timestamp}, - Message: "Git repositories are in sync", - })) - }) - It("updates lastUpdate field when condition still occurs while condition is active", func() { - var p api.Pattern - firstTimestamp := time.Time{}.Add(1 * time.Second) - By("calling the update pattern conditions to add the condition") - e := updatePatternConditions(k8sClient, api.GitInSync, foo, defaultNamespace, firstTimestamp) - Expect(e).NotTo(HaveOccurred()) - By("calling the update pattern conditions again to trigger the update of the lastUpdate field") - secondTimeStamp := time.Time{}.Add(2 * time.Second) - e = updatePatternConditions(k8sClient, api.GitInSync, foo, defaultNamespace, secondTimeStamp) - Expect(e).NotTo(HaveOccurred()) - By("retrieving the pattern object") - err := k8sClient.Get(ctx, types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &p) - Expect(err).NotTo(HaveOccurred()) - Expect(p).NotTo(BeNil()) - Expect(p.Status.Conditions).To(HaveLen(1)) - Expect(p.Status.Conditions[0]).To(BeComparableTo(api.PatternCondition{ - Type: api.GitInSync, - Status: v1core.ConditionTrue, - LastUpdateTime: v1.Time{Time: secondTimeStamp}, - LastTransitionTime: v1.Time{Time: firstTimestamp}, - Message: "Git repositories are in sync", - })) - }) - It("transitions to a new condition type as status true", func() { - var p api.Pattern - firstTimestamp := time.Time{}.Add(1 * time.Second) - By("calling the update pattern conditions to add the condition") - e := updatePatternConditions(k8sClient, api.GitInSync, foo, defaultNamespace, firstTimestamp) - Expect(e).NotTo(HaveOccurred()) - By("calling the update pattern conditions again to trigger the update of the lastUpdate field") - secondTimeStamp := time.Time{}.Add(2 * time.Second) - e = updatePatternConditions(k8sClient, api.GitOutOfSync, foo, defaultNamespace, secondTimeStamp) - Expect(e).NotTo(HaveOccurred()) - By("retrieving the pattern object") - err := k8sClient.Get(ctx, types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &p) - Expect(err).NotTo(HaveOccurred()) - Expect(p).NotTo(BeNil()) - Expect(p.Status.Conditions).To(HaveLen(2)) - Expect(p.Status.Conditions[0]).To(BeComparableTo(api.PatternCondition{ - Type: api.GitInSync, - Status: v1core.ConditionFalse, - LastUpdateTime: v1.Time{Time: secondTimeStamp}, - LastTransitionTime: v1.Time{Time: firstTimestamp}, - Message: "Git repositories are in sync", - })) - Expect(p.Status.Conditions[1]).To(BeComparableTo(api.PatternCondition{ - Type: api.GitOutOfSync, - Status: v1core.ConditionTrue, - LastUpdateTime: v1.Time{Time: secondTimeStamp}, - LastTransitionTime: v1.Time{Time: secondTimeStamp}, - Message: "Git repositories are out of sync", - })) - }) - It("transitions back to an existing condition type", func() { - var p api.Pattern - firstTimestamp := time.Time{}.Add(1 * time.Second) - By("calling the update pattern conditions to add the condition") - e := updatePatternConditions(k8sClient, api.GitInSync, foo, defaultNamespace, firstTimestamp) - Expect(e).NotTo(HaveOccurred()) - By("calling the update pattern conditions again to trigger the update of the lastUpdate field") - secondTimeStamp := time.Time{}.Add(2 * time.Second) - e = updatePatternConditions(k8sClient, api.GitOutOfSync, foo, defaultNamespace, secondTimeStamp) - Expect(e).NotTo(HaveOccurred()) - thirdTimeStamp := time.Time{}.Add(3 * time.Second) - e = updatePatternConditions(k8sClient, api.GitInSync, foo, defaultNamespace, thirdTimeStamp) - Expect(e).NotTo(HaveOccurred()) - By("retrieving the pattern object") - err := k8sClient.Get(ctx, types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &p) - Expect(err).NotTo(HaveOccurred()) - Expect(p).NotTo(BeNil()) - Expect(p.Status.Conditions).To(HaveLen(2)) - Expect(p.Status.Conditions[0]).To(BeComparableTo(api.PatternCondition{ - Type: api.GitInSync, - Status: v1core.ConditionTrue, - LastUpdateTime: v1.Time{Time: thirdTimeStamp}, - LastTransitionTime: v1.Time{Time: thirdTimeStamp}, - Message: "Git repositories are in sync", - })) - Expect(p.Status.Conditions[1]).To(BeComparableTo(api.PatternCondition{ - Type: api.GitOutOfSync, - Status: v1core.ConditionFalse, - LastUpdateTime: v1.Time{Time: thirdTimeStamp}, - LastTransitionTime: v1.Time{Time: secondTimeStamp}, - Message: "Git repositories are out of sync", - })) - }) - }) - -}) - -var _ = Describe("Drift watcher", func() { - var _ = Context("When watching for drifts", func() { - var ( - patternFoo *api.Pattern - ctrl *gomock.Controller - mockGitClient *MockGitClient - mockRemoteOrigin, mockRemoteTarget *MockRemoteClient - ) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - - mockGitClient = NewMockGitClient(ctrl) - mockRemoteOrigin = NewMockRemoteClient(ctrl) - mockRemoteTarget = NewMockRemoteClient(ctrl) - // Add the pattern in etcd - patternFoo = &api.Pattern{ - ObjectMeta: v1.ObjectMeta{Name: foo, Namespace: defaultNamespace}, - TypeMeta: v1.TypeMeta{Kind: "Pattern", APIVersion: api.GroupVersion.String()}, - Spec: api.PatternSpec{GitConfig: api.GitConfig{OriginRepo: originURL, TargetRepo: targetURL}}} - - err := k8sClient.Create(context.TODO(), patternFoo) - Expect(err).NotTo(HaveOccurred()) - - }) - - AfterEach(func() { - - err := k8sClient.Delete(context.TODO(), patternFoo) - Expect(err).NotTo(HaveOccurred()) - }) - - It("detects a drift between a pair of git repositories after the second check", func() { - var ( - payloadDelivered bool - ) - - mockGitClient.EXPECT().NewRemoteClient(gomock.Any()).DoAndReturn(func(c *config.RemoteConfig) RemoteClient { - if c.Name == "origin" { - return mockRemoteOrigin - } - return mockRemoteTarget - }).AnyTimes() - - mockRemoteOrigin.EXPECT().List(gomock.Any()).Return(firstCommitReference, nil).AnyTimes() - mockRemoteTarget.EXPECT().List(gomock.Any()).DoAndReturn(func(_ *git.ListOptions) ([]*plumbing.Reference, error) { - if !payloadDelivered { - payloadDelivered = true - return firstCommitReference, nil - } - return multipleCommitsWithDifferentHashReference, nil - }).AnyTimes() - watch, closeCh := newDriftWatcher(k8sClient, logr.New(log.NullLogSink{}), mockGitClient) - - // Add the pair - timestamp := time.Now() - err := watch.add(foo, defaultNamespace, 1) - Expect(err).NotTo(HaveOccurred()) - Eventually(func() bool { - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: foo, Namespace: defaultNamespace}, patternFoo) - Expect(err).NotTo(HaveOccurred()) - return len(patternFoo.Status.Conditions) == 1 - }).WithPolling(time.Second).WithTimeout(10*time.Second).Should(BeTrue(), "expected number of conditions %d but found %d", 1, len(patternFoo.Status.Conditions)) - // check that the conditions reflect the drift polling - Expect(patternFoo.Status.Conditions[0].Type).To(Equal(api.GitInSync)) - Expect(patternFoo.Status.Conditions[0].Status).To(Equal(v1core.ConditionTrue)) - Expect(patternFoo.Status.Conditions[0].LastUpdateTime.Time).To(BeTemporally(">", timestamp)) - Expect(patternFoo.Status.Conditions[0].LastTransitionTime.Time).To(BeTemporally(">", timestamp)) - // wait for the second check to report the drift - Eventually(func() bool { - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: foo, Namespace: defaultNamespace}, patternFoo) - Expect(err).NotTo(HaveOccurred()) - return len(patternFoo.Status.Conditions) == 2 - }).WithPolling(time.Second).WithTimeout(10*time.Second).Should(BeTrue(), "expected number of conditions %d but found %d", 2, len(patternFoo.Status.Conditions)) - // notify the routine that we're closing so that it doesn't keep checking for more drifts - close(closeCh) - // retrieve the first element in the slice - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: foo, Namespace: defaultNamespace}, patternFoo) - Expect(err).NotTo(HaveOccurred()) - - // previous condition should have status false - Expect(patternFoo.Status.Conditions[0].Type).To(Equal(api.GitInSync)) - Expect(patternFoo.Status.Conditions[0].Status).To(Equal(v1core.ConditionFalse)) - Expect(patternFoo.Status.Conditions[0].LastUpdateTime.Time).To(BeTemporally("==", patternFoo.Status.Conditions[1].LastUpdateTime.Time)) - Expect(patternFoo.Status.Conditions[0].LastTransitionTime.Time).To(BeTemporally("==", patternFoo.Status.Conditions[0].LastUpdateTime.Time.Add(-1*time.Second))) - // new condition should show the repositories have drifted - Expect(patternFoo.Status.Conditions[1].Type).To(Equal(api.GitOutOfSync)) - Expect(patternFoo.Status.Conditions[1].Status).To(Equal(v1core.ConditionTrue)) - Expect(patternFoo.Status.Conditions[1].LastTransitionTime.Time).To(BeTemporally("==", patternFoo.Status.Conditions[1].LastUpdateTime.Time)) - Expect(patternFoo.Status.Conditions[1].LastUpdateTime.Time).To(BeTemporally("==", patternFoo.Status.Conditions[0].LastTransitionTime.Time.Add(time.Second))) - }) - - }) - var _ = Context("when evaluating the processing order", func() { - var ( - mockGitClient *MockGitClient - mockRemote *MockRemoteClient - patternBar, patternFoo *api.Pattern - ctrl *gomock.Controller - ) - - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - mockGitClient = NewMockGitClient(ctrl) - mockRemote = NewMockRemoteClient(ctrl) - - patternFoo = &api.Pattern{ - ObjectMeta: v1.ObjectMeta{Name: foo, Namespace: defaultNamespace}, - TypeMeta: v1.TypeMeta{Kind: "Pattern", APIVersion: api.GroupVersion.String()}, - Spec: api.PatternSpec{GitConfig: api.GitConfig{OriginRepo: originURL, TargetRepo: targetURL}}} - patternBar = &api.Pattern{ - ObjectMeta: v1.ObjectMeta{Name: bar, Namespace: defaultNamespace}, - TypeMeta: v1.TypeMeta{Kind: "Pattern", APIVersion: api.GroupVersion.String()}, - Spec: api.PatternSpec{GitConfig: api.GitConfig{OriginRepo: originURL, TargetRepo: targetURL}}} - e := k8sClient.Create(context.Background(), patternFoo) - Expect(e).NotTo(HaveOccurred()) - e = k8sClient.Create(context.Background(), patternBar) - Expect(e).NotTo(HaveOccurred()) - }) - - AfterEach(func() { - err := k8sClient.Delete(context.TODO(), patternFoo) - Expect(err).NotTo(HaveOccurred()) - err = k8sClient.Delete(context.TODO(), patternBar) - Expect(err).NotTo(HaveOccurred()) - }) - - It("processes two pairs of git repositories in order of shortest interval", func() { - mockGitClient.EXPECT().NewRemoteClient(gomock.Any()).Return(mockRemote).AnyTimes() - mockRemote.EXPECT().List(gomock.Any()).Return(firstCommitReference, nil).AnyTimes() - - watch := newWatcher(mockGitClient) - watch.watch() - - // Add both reference pairs and wait for the drift evaluation to kick in and add the first condition - err := watch.add(foo, defaultNamespace, 5) - Expect(err).NotTo(HaveOccurred()) - err = watch.add(bar, defaultNamespace, 1) - Expect(err).NotTo(HaveOccurred()) - // check the order of processing pairs - Expect(watch.repoPairs[0].name).To(Equal(bar)) - Expect(watch.repoPairs[1].name).To(Equal(foo)) - Eventually(func() bool { - var pFoo, pBar api.Pattern - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &pFoo) - Expect(err).NotTo(HaveOccurred()) - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: bar, Namespace: defaultNamespace}, &pBar) - Expect(err).NotTo(HaveOccurred()) - return len(pFoo.Status.Conditions) == 0 && len(pBar.Status.Conditions) == 1 - }).WithPolling(time.Second).WithTimeout(10*time.Second).Should(BeTrue(), - "expected number of conditions for foo %d and bar %d but found %d and %d respectively ", 0, len(patternFoo.Status.Conditions), 1, len(patternBar.Status.Conditions)) - // Confirm the status contains a new condition with type git in sync - var pattern api.Pattern - err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: bar, Namespace: defaultNamespace}, &pattern) - Expect(err).NotTo(HaveOccurred()) - // check that the conditions reflect the drift polling - Expect(pattern.Status.Conditions[0].Type).To(Equal(api.GitInSync)) - Expect(pattern.Status.Conditions[0].Status).To(Equal(v1core.ConditionTrue)) - }) - It("removes the fist pair and adds it again with longer interval to ensure it is requeued the last", func() { - mockGitClient.EXPECT().NewRemoteClient(gomock.Any()).Return(mockRemote).AnyTimes() - mockRemote.EXPECT().List(gomock.Any()).Return(firstCommitReference, nil).AnyTimes() - - watch := newWatcher(mockGitClient) - watch.watch() - // Add both reference pairs and wait for the drift evaluation to kick in and add the first condition - err := watch.add(foo, defaultNamespace, 5) - Expect(err).NotTo(HaveOccurred()) - err = watch.add(bar, defaultNamespace, 4) - Expect(err).NotTo(HaveOccurred()) - // remove the first element - err = watch.remove(bar, defaultNamespace) - Expect(err).NotTo(HaveOccurred()) - // readd the first element but with longer interval - err = watch.add(bar, defaultNamespace, 5) - Expect(err).NotTo(HaveOccurred()) - // check the order of processing pairs - Expect(watch.repoPairs[0].name).To(Equal(foo)) - Expect(watch.repoPairs[1].name).To(Equal(bar)) - // wait for the first element to be processed at least once - var pattern api.Pattern - Eventually(func() bool { - err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &pattern) - Expect(err).NotTo(HaveOccurred()) - return len(pattern.Status.Conditions) == 1 - }).WithPolling(time.Second).WithTimeout(10*time.Second).Should(BeTrue(), "expected number of conditions to be %d but found %d", 1, len(pattern.Status.Conditions)) - }) - - It("updates the interval of an existing repository pair", func() { - mockGitClient.EXPECT().NewRemoteClient(gomock.Any()).Return(mockRemote).AnyTimes() - mockRemote.EXPECT().List(gomock.Any()).Return(firstCommitReference, nil).AnyTimes() - - watch := newWatcher(mockGitClient) - watch.watch() - // Add both reference pairs and wait for the drift evaluation to kick in and add the first condition - err := watch.add(foo, defaultNamespace, 5) - Expect(err).NotTo(HaveOccurred()) - err = watch.add(bar, defaultNamespace, 4) - Expect(err).NotTo(HaveOccurred()) - // update the first element but with longer interval - err = watch.updateInterval(bar, defaultNamespace, 6) - Expect(err).NotTo(HaveOccurred()) - // check the order of processing pairs - Expect(watch.repoPairs[0].name).To(Equal(foo)) - Expect(watch.repoPairs[1].name).To(Equal(bar)) - // wait for the first element to be processed at least once - var pattern api.Pattern - Eventually(func() bool { - err := k8sClient.Get(context.TODO(), types.NamespacedName{Name: foo, Namespace: defaultNamespace}, &pattern) - Expect(err).NotTo(HaveOccurred()) - return len(pattern.Status.Conditions) == 1 - }).WithPolling(time.Second).WithTimeout(10*time.Second).Should(BeTrue(), "expected number of conditions to be %d but found %d", 1, len(pattern.Status.Conditions)) - }) - }) - - var _ = Context("when running in parallel", func() { - const ( - defaultNamespace = "default" - ) - var ( - mockGitClient *MockGitClient - mockRemote *MockRemoteClient - ctrl *gomock.Controller - ) - BeforeEach(func() { - ctrl = gomock.NewController(GinkgoT()) - mockGitClient = NewMockGitClient(ctrl) - mockRemote = NewMockRemoteClient(ctrl) - // add references - for i := 0; i < 1000; i++ { - p := &api.Pattern{ - ObjectMeta: v1.ObjectMeta{Name: fmt.Sprintf("load-%d", i), Namespace: defaultNamespace}, - TypeMeta: v1.TypeMeta{Kind: "Pattern", APIVersion: api.GroupVersion.String()}, - Spec: api.PatternSpec{GitConfig: api.GitConfig{OriginRepo: originURL, TargetRepo: targetURL}}} - e := k8sClient.Create(context.Background(), p) - Expect(e).NotTo(HaveOccurred()) - } - - }) - - AfterEach(func() { - // add references - for i := 0; i < 1000; i++ { - p := &api.Pattern{ - ObjectMeta: v1.ObjectMeta{Name: fmt.Sprintf("load-%d", i), Namespace: defaultNamespace}, - TypeMeta: v1.TypeMeta{Kind: "Pattern", APIVersion: api.GroupVersion.String()}} - e := k8sClient.Delete(context.Background(), p) - Expect(e).NotTo(HaveOccurred()) - } - }) - It("adds,removes and check for existing pairs in parallel load with random intervals", func() { - mockGitClient.EXPECT().NewRemoteClient(gomock.Any()).Return(mockRemote).AnyTimes() - mockRemote.EXPECT().List(gomock.Any()).Return(firstCommitReference, nil).AnyTimes() - - watch, _ := newDriftWatcher(k8sClient, logr.New(log.NullLogSink{}), mockGitClient) - wg := sync.WaitGroup{} - wg.Add(2) - go func() { - for i := 0; i < 1000; i++ { - // set interval between 1-2 seconds to force the trigger of the timer function during the test - interval := rand.Intn(2) + 1 //nolint:gosec - name := fmt.Sprintf("load-%d", rand.Intn(1000)) //nolint:gosec - for watch.isWatching(name, defaultNamespace) { - name = fmt.Sprintf("load-%d", rand.Intn(1000)) //nolint:gosec - } - Expect(watch.add(name, defaultNamespace, interval)).NotTo(HaveOccurred()) - } - wg.Done() - }() - go func() { - var deleted int - for deleted < 1000 { - name := fmt.Sprintf("load-%d", rand.Intn(1000)) //nolint:gosec - if watch.isWatching(name, defaultNamespace) { - Expect(watch.remove(name, defaultNamespace)).NotTo(HaveOccurred()) - deleted++ - } - } - wg.Done() - }() - wg.Wait() - }) - }) -}) - -func newWatcher(gitClient GitClient) *watcher { - return &watcher{ - kClient: k8sClient, - repoPairs: repositoryPairs{}, - endCh: make(chan any), - mutex: &sync.Mutex{}, - gitClient: gitClient, - logger: logr.New(log.NullLogSink{}), - } -} diff --git a/internal/controller/mock_drift.go b/internal/controller/mock_drift.go deleted file mode 100644 index 5c69d1bfb..000000000 --- a/internal/controller/mock_drift.go +++ /dev/null @@ -1,186 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: controllers/drift.go -// -// Generated by this command: -// -// mockgen -source controllers/drift.go -package controllers -self_package=github.com/hybrid-cloud-patterns/patterns-operator/controllers -// -// Package controllers is a generated GoMock package. -package controllers - -import ( - reflect "reflect" - - v5 "github.com/go-git/go-git/v5" - config "github.com/go-git/go-git/v5/config" - plumbing "github.com/go-git/go-git/v5/plumbing" - gomock "go.uber.org/mock/gomock" -) - -// MockRemoteClient is a mock of RemoteClient interface. -type MockRemoteClient struct { - ctrl *gomock.Controller - recorder *MockRemoteClientMockRecorder -} - -// MockRemoteClientMockRecorder is the mock recorder for MockRemoteClient. -type MockRemoteClientMockRecorder struct { - mock *MockRemoteClient -} - -// NewMockRemoteClient creates a new mock instance. -func NewMockRemoteClient(ctrl *gomock.Controller) *MockRemoteClient { - mock := &MockRemoteClient{ctrl: ctrl} - mock.recorder = &MockRemoteClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockRemoteClient) EXPECT() *MockRemoteClientMockRecorder { - return m.recorder -} - -// List mocks base method. -func (m *MockRemoteClient) List(o *v5.ListOptions) ([]*plumbing.Reference, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List", o) - ret0, _ := ret[0].([]*plumbing.Reference) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// List indicates an expected call of List. -func (mr *MockRemoteClientMockRecorder) List(o any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRemoteClient)(nil).List), o) -} - -// MockGitClient is a mock of GitClient interface. -type MockGitClient struct { - ctrl *gomock.Controller - recorder *MockGitClientMockRecorder -} - -// MockGitClientMockRecorder is the mock recorder for MockGitClient. -type MockGitClientMockRecorder struct { - mock *MockGitClient -} - -// NewMockGitClient creates a new mock instance. -func NewMockGitClient(ctrl *gomock.Controller) *MockGitClient { - mock := &MockGitClient{ctrl: ctrl} - mock.recorder = &MockGitClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockGitClient) EXPECT() *MockGitClientMockRecorder { - return m.recorder -} - -// NewRemoteClient mocks base method. -func (m *MockGitClient) NewRemoteClient(c *config.RemoteConfig) RemoteClient { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewRemoteClient", c) - ret0, _ := ret[0].(RemoteClient) - return ret0 -} - -// NewRemoteClient indicates an expected call of NewRemoteClient. -func (mr *MockGitClientMockRecorder) NewRemoteClient(c any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewRemoteClient", reflect.TypeOf((*MockGitClient)(nil).NewRemoteClient), c) -} - -// MockdriftWatcher is a mock of driftWatcher interface. -type MockdriftWatcher struct { - ctrl *gomock.Controller - recorder *MockdriftWatcherMockRecorder -} - -// MockdriftWatcherMockRecorder is the mock recorder for MockdriftWatcher. -type MockdriftWatcherMockRecorder struct { - mock *MockdriftWatcher -} - -// NewMockdriftWatcher creates a new mock instance. -func NewMockdriftWatcher(ctrl *gomock.Controller) *MockdriftWatcher { - mock := &MockdriftWatcher{ctrl: ctrl} - mock.recorder = &MockdriftWatcherMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockdriftWatcher) EXPECT() *MockdriftWatcherMockRecorder { - return m.recorder -} - -// add mocks base method. -func (m *MockdriftWatcher) add(name, namespace string, interval int) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "add", name, namespace, interval) - ret0, _ := ret[0].(error) - return ret0 -} - -// add indicates an expected call of add. -func (mr *MockdriftWatcherMockRecorder) add(name, namespace, interval any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "add", reflect.TypeOf((*MockdriftWatcher)(nil).add), name, namespace, interval) -} - -// isWatching mocks base method. -func (m *MockdriftWatcher) isWatching(name, namespace string) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "isWatching", name, namespace) - ret0, _ := ret[0].(bool) - return ret0 -} - -// isWatching indicates an expected call of isWatching. -func (mr *MockdriftWatcherMockRecorder) isWatching(name, namespace any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isWatching", reflect.TypeOf((*MockdriftWatcher)(nil).isWatching), name, namespace) -} - -// remove mocks base method. -func (m *MockdriftWatcher) remove(name, namespace string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "remove", name, namespace) - ret0, _ := ret[0].(error) - return ret0 -} - -// remove indicates an expected call of remove. -func (mr *MockdriftWatcherMockRecorder) remove(name, namespace any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "remove", reflect.TypeOf((*MockdriftWatcher)(nil).remove), name, namespace) -} - -// updateInterval mocks base method. -func (m *MockdriftWatcher) updateInterval(name, namespace string, interval int) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "updateInterval", name, namespace, interval) - ret0, _ := ret[0].(error) - return ret0 -} - -// updateInterval indicates an expected call of updateInterval. -func (mr *MockdriftWatcherMockRecorder) updateInterval(name, namespace, interval any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "updateInterval", reflect.TypeOf((*MockdriftWatcher)(nil).updateInterval), name, namespace, interval) -} - -// watch mocks base method. -func (m *MockdriftWatcher) watch() chan any { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "watch") - ret0, _ := ret[0].(chan any) - return ret0 -} - -// watch indicates an expected call of watch. -func (mr *MockdriftWatcherMockRecorder) watch() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "watch", reflect.TypeOf((*MockdriftWatcher)(nil).watch)) -} diff --git a/internal/controller/pattern_controller.go b/internal/controller/pattern_controller.go index 1fcde5f4b..a7230d8b8 100644 --- a/internal/controller/pattern_controller.go +++ b/internal/controller/pattern_controller.go @@ -69,7 +69,6 @@ type PatternReconciler struct { dynamicClient dynamic.Interface routeClient routeclient.Interface operatorClient operatorclient.OperatorV1Interface - driftWatcher driftWatcher gitOperations GitOperations giteaOperations GiteaOperations } @@ -161,32 +160,6 @@ func (r *PatternReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return r.actionPerformed(qualifiedInstance, "Updated status with start event sent", nil) } - gitConfig := qualifiedInstance.Spec.GitConfig - // -- Git Drift monitoring - // if both git repositories are defined in the pattern's git configuration and the polling interval is not set to disable watching - if gitConfig.OriginRepo != "" && gitConfig.TargetRepo != "" && gitConfig.PollInterval != -1 { - if !r.driftWatcher.isWatching(qualifiedInstance.Name, qualifiedInstance.Namespace) { - // start monitoring drifts for this pattern - err = r.driftWatcher.add(qualifiedInstance.Name, - qualifiedInstance.Namespace, - gitConfig.PollInterval) - if err != nil { - return r.actionPerformed(qualifiedInstance, "add pattern to git drift watcher", err) - } - } else { - err = r.driftWatcher.updateInterval(qualifiedInstance.Name, qualifiedInstance.Namespace, gitConfig.PollInterval) - if err != nil { - return r.actionPerformed(qualifiedInstance, "update the watch interval to git drift watcher", err) - } - } - } else if r.driftWatcher.isWatching(qualifiedInstance.Name, qualifiedInstance.Namespace) { - // The pattern has been updated an it no longer fulfills the conditions to monitor the drift - err = r.driftWatcher.remove(qualifiedInstance.Name, qualifiedInstance.Namespace) - if err != nil { - return r.actionPerformed(qualifiedInstance, "remove pattern from git drift watcher", err) - } - } - // -- GitOps Subscription targetSub, _ := newSubscriptionFromConfigMap(r.fullClient) _ = controllerutil.SetOwnerReference(qualifiedInstance, targetSub, r.Scheme) @@ -238,7 +211,7 @@ func (r *PatternReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } // If you specified OriginRepo then we automatically spawn a gitea instance via a special argo gitea application - if gitConfig.OriginRepo != "" { + if qualifiedInstance.Spec.GitConfig.OriginRepo != "" { giteaErr := r.createGiteaInstance(qualifiedInstance) if giteaErr != nil { return r.actionPerformed(qualifiedInstance, "error created gitea instance", giteaErr) @@ -514,12 +487,6 @@ func (r *PatternReconciler) applyDefaults(input *api.Pattern) (*api.Pattern, err output.Spec.MultiSourceConfig.HelmRepoUrl = "https://charts.validatedpatterns.io/" } - // interval cannot be less than 180 seconds to avoid drowning the API server in requests - // value of -1 effectively disables the watch for this pattern. - if output.Spec.GitConfig.PollInterval > -1 && output.Spec.GitConfig.PollInterval < 180 { - output.Spec.GitConfig.PollInterval = 180 - } - localCheckoutPath := getLocalGitPath(output.Spec.GitConfig.TargetRepo) if localCheckoutPath != output.Status.LocalCheckoutPath { _ = DropLocalGitPaths() @@ -557,12 +524,6 @@ func (r *PatternReconciler) finalizeObject(instance *api.Pattern) error { return nil } - if r.driftWatcher.isWatching(qualifiedInstance.Name, qualifiedInstance.Namespace) { - // Stop watching for drifts in the pattern's git repositories - if err := r.driftWatcher.remove(instance.Name, instance.Namespace); err != nil { - return err - } - } if changed, _ := updateApplication(r.argoClient, targetApp, app, ns); changed { return fmt.Errorf("updated application %q for removal", app.Name) } @@ -617,7 +578,6 @@ func (r *PatternReconciler) SetupWithManager(mgr ctrl.Manager) error { if r.routeClient, err = routeclient.NewForConfig(r.config); err != nil { return err } - r.driftWatcher, _ = newDriftWatcher(r.Client, mgr.GetLogger(), newGitClient()) r.gitOperations = &GitOperationsImpl{} r.giteaOperations = &GiteaOperationsImpl{} diff --git a/internal/controller/pattern_controller_test.go b/internal/controller/pattern_controller_test.go index a5232adcd..74bfd6f65 100644 --- a/internal/controller/pattern_controller_test.go +++ b/internal/controller/pattern_controller_test.go @@ -19,7 +19,6 @@ package controllers import ( "context" "os" - "time" "github.com/go-git/go-git/v5" "github.com/go-logr/logr" @@ -40,19 +39,22 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/log" ) const ( - namespace = "openshift-operators" - defaultInterval = time.Duration(180) * time.Second + namespace = "openshift-operators" + defaultNamespace = "default" + foo = "foo" + originURL = "https://origin.url" + targetURL = "https://target.url" ) var ( patternNamespaced = types.NamespacedName{Name: foo, Namespace: namespace} mockGitOps *MockGitOperations + gitOptions *git.CloneOptions ) var _ = Describe("pattern controller", func() { @@ -60,133 +62,20 @@ var _ = Describe("pattern controller", func() { var ( p *api.Pattern reconciler *PatternReconciler - watch *watcher - gitOptions *git.CloneOptions ) BeforeEach(func() { nsOperators := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} - reconciler = newFakeReconciler(nsOperators, buildPatternManifest(10)) - watch = reconciler.driftWatcher.(*watcher) + reconciler = newFakeReconciler(nsOperators, buildPatternManifest()) gitOptions = &git.CloneOptions{ - URL: "https://target.url", - Progress: os.Stdout, - Depth: 0, - // ReferenceName: plumbing.ReferenceName, + URL: "https://target.url", + Progress: os.Stdout, + Depth: 0, RemoteName: "origin", SingleBranch: false, Tags: git.AllTags, } }) - It("adding a pattern with origin, target and interval >-1", func() { - By("adding the pattern to the watch") - mockGitOps.EXPECT().CloneRepository("/tmp/vp/https___target.url", false, gitOptions).Return(nil, nil) - mockGitOps.EXPECT().OpenRepository("/tmp/vp/https___target.url").Return(nil, nil) - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(HaveLen(1)) - Expect(watch.repoPairs[0].name).To(Equal(foo)) - Expect(watch.repoPairs[0].namespace).To(Equal(namespace)) - Expect(watch.repoPairs[0].interval).To(Equal(defaultInterval)) - }) - - It("adding a pattern without origin Repository", func() { - p = &api.Pattern{} - mockGitOps.EXPECT().CloneRepository("/tmp/vp/https___target.url", false, gitOptions).Return(nil, nil) - mockGitOps.EXPECT().OpenRepository("/tmp/vp/https___target.url").Return(nil, nil) - err := reconciler.Client.Get(context.Background(), patternNamespaced, p) - Expect(err).NotTo(HaveOccurred()) - p.Spec.GitConfig.OriginRepo = "" - err = reconciler.Client.Update(context.Background(), p) - Expect(err).NotTo(HaveOccurred()) - - By("validating the watch slice is empty") - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(BeEmpty()) - }) - - It("adding a pattern with interval == -1", func() { - p = &api.Pattern{} - mockGitOps.EXPECT().CloneRepository("/tmp/vp/https___target.url", false, gitOptions).Return(nil, nil) - mockGitOps.EXPECT().OpenRepository("/tmp/vp/https___target.url").Return(nil, nil) - err := reconciler.Client.Get(context.Background(), patternNamespaced, p) - Expect(err).NotTo(HaveOccurred()) - p.Spec.GitConfig.PollInterval = -1 - err = reconciler.Client.Update(context.Background(), p) - Expect(err).NotTo(HaveOccurred()) - - By("validating the watch slice is empty") - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(BeEmpty()) - }) - - It("validates changes to the poll interval in the manifest", func() { - mockGitOps.EXPECT().CloneRepository("/tmp/vp/https___target.url", false, gitOptions).Return(nil, nil).AnyTimes() - mockGitOps.EXPECT().OpenRepository("/tmp/vp/https___target.url").Return(nil, nil).AnyTimes() - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(HaveLen(1)) - - By("updating the pattern's interval") - p = &api.Pattern{} - err := reconciler.Client.Get(context.Background(), patternNamespaced, p) - Expect(err).NotTo(HaveOccurred()) - p.Spec.GitConfig.PollInterval = 200 - err = reconciler.Client.Update(context.Background(), p) - Expect(err).NotTo(HaveOccurred()) - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(HaveLen(1)) - Expect(watch.repoPairs[0].name).To(Equal(foo)) - Expect(watch.repoPairs[0].namespace).To(Equal(namespace)) - Expect(watch.repoPairs[0].interval).To(Equal(time.Duration(200) * time.Second)) - - By("disabling the watch by updating the interval to be -1") - err = reconciler.Client.Get(context.Background(), patternNamespaced, p) - Expect(err).NotTo(HaveOccurred()) - p.Spec.GitConfig.PollInterval = -1 - err = reconciler.Client.Update(context.Background(), p) - Expect(err).NotTo(HaveOccurred()) - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(BeEmpty()) - By("reenabling the watch by setting the interval to a value greater than 0 but below the minimum interval of 180 seconds") - err = reconciler.Client.Get(context.Background(), patternNamespaced, p) - Expect(err).NotTo(HaveOccurred()) - p.Spec.GitConfig.PollInterval = 100 - err = reconciler.Client.Update(context.Background(), p) - Expect(err).NotTo(HaveOccurred()) - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(HaveLen(1)) - Expect(watch.repoPairs[0].name).To(Equal(foo)) - Expect(watch.repoPairs[0].namespace).To(Equal(namespace)) - Expect(watch.repoPairs[0].interval).To(Equal(defaultInterval)) - }) - - It("removes an existing pattern from the drift watcher by changing the originRepository to empty", func() { - mockGitOps.EXPECT().CloneRepository("/tmp/vp/https___target.url", false, gitOptions).Return(nil, nil).AnyTimes() - mockGitOps.EXPECT().OpenRepository("/tmp/vp/https___target.url").Return(nil, nil).AnyTimes() - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(HaveLen(1)) - - By("disabling the watch by updating the originRepository to be empty") - p = &api.Pattern{} - err := reconciler.Client.Get(context.Background(), patternNamespaced, p) - Expect(err).NotTo(HaveOccurred()) - p.Spec.GitConfig.OriginRepo = "" - err = reconciler.Client.Update(context.Background(), p) - Expect(err).NotTo(HaveOccurred()) - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(BeEmpty()) - By("reenabling the watch by resetting the originRepository value") - err = reconciler.Client.Get(context.Background(), patternNamespaced, p) - Expect(err).NotTo(HaveOccurred()) - p.Spec.GitConfig.OriginRepo = originURL - err = reconciler.Client.Update(context.Background(), p) - Expect(err).NotTo(HaveOccurred()) - _, _ = reconciler.Reconcile(context.Background(), ctrl.Request{NamespacedName: patternNamespaced}) - Expect(watch.repoPairs).To(HaveLen(1)) - Expect(watch.repoPairs[0].name).To(Equal(foo)) - Expect(watch.repoPairs[0].namespace).To(Equal(namespace)) - Expect(watch.repoPairs[0].interval).To(Equal(defaultInterval)) - }) - It("adding a pattern with application status", func() { p = &api.Pattern{} err := reconciler.Client.Get(context.Background(), patternNamespaced, p) @@ -226,12 +115,10 @@ func newFakeReconciler(initObjects ...runtime.Object) *PatternReconciler { Spec: operatorv1.OpenShiftControllerManagerSpec{}, Status: operatorv1.OpenShiftControllerManagerStatus{OperatorStatus: operatorv1.OperatorStatus{Version: "4.10.3"}}} ingress := &v1.Ingress{ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, Spec: v1.IngressSpec{Domain: "hello.world"}} - watcher, _ := newDriftWatcher(fakeClient, logr.New(log.NullLogSink{}), newGitClient()) return &PatternReconciler{ Scheme: scheme.Scheme, Client: fakeClient, olmClient: olmclient.NewSimpleClientset(), - driftWatcher: watcher, fullClient: kubeclient.NewSimpleClientset(), configClient: configclient.NewSimpleClientset(clusterVersion, clusterInfra, ingress), operatorClient: operatorclient.NewSimpleClientset(osControlManager).OperatorV1(), @@ -240,7 +127,7 @@ func newFakeReconciler(initObjects ...runtime.Object) *PatternReconciler { } } -func buildPatternManifest(interval int) *api.Pattern { +func buildPatternManifest() *api.Pattern { return &api.Pattern{ObjectMeta: metav1.ObjectMeta{ Name: foo, Namespace: namespace, @@ -248,9 +135,8 @@ func buildPatternManifest(interval int) *api.Pattern { }, Spec: api.PatternSpec{ GitConfig: api.GitConfig{ - OriginRepo: originURL, - TargetRepo: targetURL, - PollInterval: interval, + OriginRepo: originURL, + TargetRepo: targetURL, }, }, Status: api.PatternStatus{