1972 lines
69 KiB
Go
1972 lines
69 KiB
Go
/*
|
|
Copyright 2025 The Flux authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
|
extgogit "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-git/go-git/v5/plumbing/object"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/otiai10/copy"
|
|
corev1 "k8s.io/api/core/v1"
|
|
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/rand"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
"k8s.io/client-go/tools/record"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
|
|
|
imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
|
aclapi "github.com/fluxcd/pkg/apis/acl"
|
|
"github.com/fluxcd/pkg/apis/meta"
|
|
"github.com/fluxcd/pkg/gittestserver"
|
|
"github.com/fluxcd/pkg/runtime/conditions"
|
|
conditionscheck "github.com/fluxcd/pkg/runtime/conditions/check"
|
|
"github.com/fluxcd/pkg/runtime/patch"
|
|
"github.com/fluxcd/pkg/ssh"
|
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
|
|
|
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
|
"github.com/fluxcd/image-automation-controller/internal/source"
|
|
"github.com/fluxcd/image-automation-controller/internal/testutil"
|
|
"github.com/fluxcd/image-automation-controller/pkg/test"
|
|
)
|
|
|
|
const (
|
|
originRemote = "origin"
|
|
timeout = 10 * time.Second
|
|
testAuthorName = "Flux B Ot"
|
|
testAuthorEmail = "fluxbot@example.com"
|
|
testCommitTemplate = `Commit summary
|
|
|
|
Automation: {{ .AutomationObject }}
|
|
|
|
Files:
|
|
{{ range $filename, $_ := .Updated.Files -}}
|
|
- {{ $filename }}
|
|
{{ end -}}
|
|
|
|
Objects:
|
|
{{ range $resource, $_ := .Updated.Objects -}}
|
|
{{ if eq $resource.Kind "Deployment" -}}
|
|
- {{ $resource.Kind | lower }} {{ $resource.Name | lower }}
|
|
{{ else -}}
|
|
- {{ $resource.Kind }} {{ $resource.Name }}
|
|
{{ end -}}
|
|
{{ end -}}
|
|
|
|
Images:
|
|
{{ range .Updated.Images -}}
|
|
- {{.}} ({{.Policy.Name}})
|
|
{{ end -}}
|
|
`
|
|
testCommitMessageFmt = `Commit summary
|
|
|
|
Automation: %s/update-test
|
|
|
|
Files:
|
|
- deploy.yaml
|
|
Objects:
|
|
- deployment test
|
|
Images:
|
|
- helloworld:v1.0.0 (%s)
|
|
`
|
|
)
|
|
|
|
func TestImageUpdateAutomationReconciler_deleteBeforeFinalizer(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "imageupdate")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
imageUpdate := &imagev1.ImageUpdateAutomation{}
|
|
imageUpdate.Name = "test-imageupdate"
|
|
imageUpdate.Namespace = namespace.Name
|
|
imageUpdate.Spec = imagev1.ImageUpdateAutomationSpec{
|
|
Interval: metav1.Duration{Duration: time.Hour},
|
|
SourceRef: imagev1.CrossNamespaceSourceReference{
|
|
Kind: "GitRepository",
|
|
Name: "foo",
|
|
},
|
|
}
|
|
// Add a test finalizer to prevent the object from getting deleted.
|
|
imageUpdate.SetFinalizers([]string{"test-finalizer"})
|
|
g.Expect(k8sClient.Create(ctx, imageUpdate)).NotTo(HaveOccurred())
|
|
// Add deletion timestamp by deleting the object.
|
|
g.Expect(k8sClient.Delete(ctx, imageUpdate)).NotTo(HaveOccurred())
|
|
|
|
r := &ImageUpdateAutomationReconciler{
|
|
Client: k8sClient,
|
|
EventRecorder: record.NewFakeRecorder(32),
|
|
}
|
|
// NOTE: Only a real API server responds with an error in this scenario.
|
|
g.Eventually(func() error {
|
|
_, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(imageUpdate)})
|
|
return err
|
|
}, timeout).Should(Succeed())
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_watchSourceAndLatestImage(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
policySpec := imagev1_reflect.ImagePolicySpec{
|
|
ImageRepositoryRef: meta.NamespacedObjectReference{
|
|
Name: "not-expected-to-exist",
|
|
},
|
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
|
Range: "1.x",
|
|
},
|
|
},
|
|
}
|
|
fixture := "testdata/appconfig"
|
|
latest := "helloworld:v1.0.0"
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(ctx, g, testEnv, namespace.Name, fixture, policySpec, latest, func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
// Update the setter marker in the repo.
|
|
policyKey := types.NamespacedName{
|
|
Name: s.imagePolicyName,
|
|
Namespace: s.namespace,
|
|
}
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
// Create the automation object.
|
|
updateStrategy := &imagev1.UpdateStrategy{
|
|
Strategy: imagev1.UpdateStrategySetters,
|
|
}
|
|
err := createImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, s.branch, "", testCommitTemplate, "", updateStrategy)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
var imageUpdate imagev1.ImageUpdateAutomation
|
|
imageUpdateKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: "update-test",
|
|
}
|
|
|
|
// Let the image update be ready.
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, imageUpdateKey, &imageUpdate); err != nil {
|
|
return false
|
|
}
|
|
return conditions.IsReady(&imageUpdate)
|
|
}, timeout).Should(BeTrue())
|
|
lastPushedCommit := imageUpdate.Status.LastPushCommit
|
|
|
|
// Update ImagePolicy with new latest and wait for image update to
|
|
// trigger.
|
|
latest = "helloworld:v1.1.0"
|
|
err = updateImagePolicyWithLatestImage(ctx, testEnv, s.imagePolicyName, s.namespace, latest)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, imageUpdateKey, &imageUpdate); err != nil {
|
|
return false
|
|
}
|
|
ready := conditions.Get(&imageUpdate, meta.ReadyCondition)
|
|
return ready.Status == metav1.ConditionTrue && imageUpdate.Status.LastPushCommit != lastPushedCommit
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Update GitRepo with bad config and wait for image update to fail.
|
|
var gitRepo sourcev1.GitRepository
|
|
gitRepoKey := types.NamespacedName{
|
|
Name: s.gitRepoName,
|
|
Namespace: s.gitRepoNamespace,
|
|
}
|
|
g.Expect(testEnv.Get(ctx, gitRepoKey, &gitRepo)).To(Succeed())
|
|
patch := client.MergeFrom(gitRepo.DeepCopy())
|
|
gitRepo.Spec.SecretRef = &meta.LocalObjectReference{Name: "non-existing-secret"}
|
|
g.Expect(testEnv.Patch(ctx, &gitRepo, patch)).To(Succeed())
|
|
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, imageUpdateKey, &imageUpdate); err != nil {
|
|
return false
|
|
}
|
|
return conditions.IsFalse(&imageUpdate, meta.ReadyCondition)
|
|
}, timeout).Should(BeTrue())
|
|
})
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_suspended(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
updateKey := types.NamespacedName{
|
|
Name: "test-update",
|
|
Namespace: "default",
|
|
}
|
|
update := &imagev1.ImageUpdateAutomation{
|
|
Spec: imagev1.ImageUpdateAutomationSpec{
|
|
Interval: metav1.Duration{Duration: time.Hour},
|
|
Suspend: true,
|
|
},
|
|
}
|
|
update.Name = updateKey.Name
|
|
update.Namespace = updateKey.Namespace
|
|
|
|
// Add finalizer so that reconciliation reaches suspend check.
|
|
controllerutil.AddFinalizer(update, imagev1.ImageUpdateAutomationFinalizer)
|
|
|
|
builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
|
|
builder.WithObjects(update)
|
|
|
|
r := ImageUpdateAutomationReconciler{
|
|
Client: builder.Build(),
|
|
}
|
|
|
|
res, err := r.Reconcile(context.TODO(), ctrl.Request{NamespacedName: updateKey})
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(res.Requeue).ToNot(BeTrue())
|
|
|
|
// Make sure no status was written.
|
|
g.Expect(r.Get(context.TODO(), updateKey, update)).To(Succeed())
|
|
g.Expect(update.Status.Conditions).To(HaveLen(0))
|
|
g.Expect(update.Status.LastAutomationRunTime).To(BeNil())
|
|
|
|
// Cleanup.
|
|
g.Expect(r.Delete(ctx, update)).To(Succeed())
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_Reconcile(t *testing.T) {
|
|
policySpec := imagev1_reflect.ImagePolicySpec{
|
|
ImageRepositoryRef: meta.NamespacedObjectReference{
|
|
Name: "not-expected-to-exist",
|
|
},
|
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
|
Range: "1.x",
|
|
},
|
|
},
|
|
}
|
|
fixture := "testdata/appconfig"
|
|
latest := "helloworld:v1.0.0"
|
|
updateName := "test-update"
|
|
|
|
t.Run("no gitspec results in stalled", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
obj := &imagev1.ImageUpdateAutomation{}
|
|
obj.Name = updateName
|
|
obj.Namespace = namespace.Name
|
|
obj.Spec = imagev1.ImageUpdateAutomationSpec{
|
|
SourceRef: imagev1.CrossNamespaceSourceReference{
|
|
Name: "non-existing",
|
|
Kind: sourcev1.GitRepositoryKind,
|
|
},
|
|
}
|
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, obj.Name, obj.Namespace)).To(Succeed())
|
|
}()
|
|
|
|
expectedConditions := []metav1.Condition{
|
|
*conditions.TrueCondition(meta.StalledCondition, imagev1.InvalidSourceConfigReason, "invalid source configuration"),
|
|
*conditions.FalseCondition(meta.ReadyCondition, imagev1.InvalidSourceConfigReason, "invalid source configuration"),
|
|
}
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
|
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
|
|
}).Should(Succeed())
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, obj)
|
|
})
|
|
|
|
t.Run("invalid policy selector results in stalled", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
obj := &imagev1.ImageUpdateAutomation{}
|
|
obj.Name = updateName
|
|
obj.Namespace = namespace.Name
|
|
obj.Spec = imagev1.ImageUpdateAutomationSpec{
|
|
SourceRef: imagev1.CrossNamespaceSourceReference{
|
|
Kind: "GitRepository",
|
|
Name: "foo",
|
|
},
|
|
PolicySelector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"label-too-long-" + strings.Repeat("0", validation.LabelValueMaxLength): "",
|
|
},
|
|
},
|
|
}
|
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, obj.Name, obj.Namespace)).To(Succeed())
|
|
}()
|
|
|
|
expectedConditions := []metav1.Condition{
|
|
*conditions.TrueCondition(meta.StalledCondition, imagev1.InvalidPolicySelectorReason, "failed to parse policy selector"),
|
|
*conditions.FalseCondition(meta.ReadyCondition, imagev1.InvalidPolicySelectorReason, "failed to parse policy selector"),
|
|
}
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
|
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
|
|
}).Should(Succeed())
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, obj)
|
|
})
|
|
|
|
t.Run("non-existing gitrepo results in failure", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
obj := &imagev1.ImageUpdateAutomation{}
|
|
obj.Name = updateName
|
|
obj.Namespace = namespace.Name
|
|
obj.Spec = imagev1.ImageUpdateAutomationSpec{
|
|
SourceRef: imagev1.CrossNamespaceSourceReference{
|
|
Name: "non-existing",
|
|
Kind: sourcev1.GitRepositoryKind,
|
|
},
|
|
GitSpec: &imagev1.GitSpec{
|
|
Commit: imagev1.CommitSpec{
|
|
Author: imagev1.CommitUser{
|
|
Email: "aaa",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
g.Expect(testEnv.Create(ctx, obj)).To(Succeed())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, obj.Name, obj.Namespace)).To(Succeed())
|
|
}()
|
|
|
|
expectedConditions := []metav1.Condition{
|
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "processing"),
|
|
*conditions.FalseCondition(meta.ReadyCondition, imagev1.SourceManagerFailedReason, "not found"),
|
|
}
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
|
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
|
|
}).Should(Succeed())
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, obj)
|
|
})
|
|
|
|
t.Run("source checkout fails", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
err := createImageUpdateAutomation(ctx, testEnv, updateName, s.namespace, s.gitRepoName, s.gitRepoNamespace, "bad-branch", s.branch, "", testCommitTemplate, "", nil)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, updateName, s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
objKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: updateName,
|
|
}
|
|
var obj imagev1.ImageUpdateAutomation
|
|
|
|
expectedConditions := []metav1.Condition{
|
|
*conditions.TrueCondition(meta.ReconcilingCondition, meta.ProgressingWithRetryReason, "processing"),
|
|
*conditions.FalseCondition(meta.ReadyCondition, imagev1.GitOperationFailedReason, "reference not found"),
|
|
}
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
|
|
}).Should(Succeed())
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, &obj)
|
|
})
|
|
})
|
|
|
|
t.Run("no marker no update", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
err := createImageUpdateAutomation(ctx, testEnv, updateName, s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, s.branch, "", testCommitTemplate, "", nil)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, updateName, s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
objKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: updateName,
|
|
}
|
|
var obj imagev1.ImageUpdateAutomation
|
|
|
|
expectedConditions := []metav1.Condition{
|
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, readyMessage),
|
|
}
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(obj.Status.ObservedGeneration).To(Equal(obj.GetGeneration()))
|
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
|
|
}).Should(Succeed())
|
|
|
|
g.Expect(obj.Status.LastPushCommit).To(BeEmpty())
|
|
g.Expect(obj.Status.LastPushTime).To(BeNil())
|
|
g.Expect(obj.Status.LastAutomationRunTime).ToNot(BeNil())
|
|
g.Expect(obj.Status.ObservedSourceRevision).ToNot(BeEmpty())
|
|
g.Expect(obj.Status.ObservedPolicies).To(HaveLen(1))
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, &obj)
|
|
})
|
|
})
|
|
|
|
t.Run("push update", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
policyKey := types.NamespacedName{
|
|
Name: s.imagePolicyName,
|
|
Namespace: s.namespace,
|
|
}
|
|
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
err := createImageUpdateAutomation(ctx, testEnv, updateName, s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, s.branch, "", testCommitTemplate, "", nil)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, updateName, s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
objKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: updateName,
|
|
}
|
|
var obj imagev1.ImageUpdateAutomation
|
|
|
|
expectedConditions := []metav1.Condition{
|
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, readyMessage),
|
|
}
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(obj.Status.ObservedGeneration).To(Equal(obj.GetGeneration()))
|
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
|
|
}).Should(Succeed())
|
|
g.Expect(obj.Status.LastPushCommit).ToNot(BeEmpty())
|
|
g.Expect(obj.Status.LastPushTime).ToNot(BeNil())
|
|
g.Expect(obj.Status.LastAutomationRunTime).ToNot(BeNil())
|
|
g.Expect(obj.Status.ObservedSourceRevision).To(ContainSubstring("%s@sha1", s.branch))
|
|
g.Expect(obj.Status.ObservedPolicies).To(HaveLen(1))
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, &obj)
|
|
})
|
|
})
|
|
|
|
t.Run("source moves forward & policy updates separately, new observations", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
policyKey1 := types.NamespacedName{
|
|
Name: s.imagePolicyName,
|
|
Namespace: s.namespace,
|
|
}
|
|
|
|
policyKey2 := types.NamespacedName{
|
|
Name: "non-existing-policy",
|
|
Namespace: s.namespace,
|
|
}
|
|
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey1)).To(Succeed())
|
|
})
|
|
|
|
err := createImageUpdateAutomation(ctx, testEnv, updateName, s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, s.branch, "", testCommitTemplate, "", nil)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, updateName, s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
objKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: updateName,
|
|
}
|
|
var obj imagev1.ImageUpdateAutomation
|
|
|
|
expectedConditions := []metav1.Condition{
|
|
*conditions.TrueCondition(meta.ReadyCondition, meta.SucceededReason, readyMessage),
|
|
}
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(obj.Status.ObservedGeneration).To(Equal(obj.GetGeneration()))
|
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(expectedConditions))
|
|
}, timeout).Should(Succeed())
|
|
g.Expect(obj.Status.LastAutomationRunTime).ToNot(BeNil())
|
|
g.Expect(obj.Status.LastPushCommit).ToNot(BeEmpty())
|
|
g.Expect(obj.Status.LastPushTime).ToNot(BeNil())
|
|
g.Expect(obj.Status.LastAutomationRunTime).ToNot(BeNil())
|
|
g.Expect(obj.Status.ObservedSourceRevision).To(ContainSubstring("%s@sha1", s.branch))
|
|
g.Expect(obj.Status.ObservedPolicies).To(HaveLen(1))
|
|
|
|
// Record the previous values and check after a reconciliation.
|
|
//
|
|
// NOTE: Ignoring LastAutomationRunTime as the recorded time is
|
|
// only up to seconds. Because the test runs really quick, the
|
|
// run time may be at the same second. Introducing a sleep for a
|
|
// second shows that the time gets updated. Avoiding to
|
|
// introduce a sleep to test this for now.
|
|
srcRevBefore := obj.Status.ObservedSourceRevision
|
|
pushCommitBefore := obj.Status.LastPushCommit
|
|
pushTimeBefore := obj.Status.LastPushTime
|
|
|
|
// Annotate the object and trigger a no-op reconciliation.
|
|
patch := client.MergeFrom(obj.DeepCopy())
|
|
obj.SetAnnotations(map[string]string{meta.ReconcileRequestAnnotation: "now"})
|
|
g.Expect(testEnv.Patch(ctx, &obj, patch)).To(Succeed())
|
|
|
|
// Look for the LastHandledReconcileAt to update.
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(conditions.IsReady(&obj)).To(BeTrue())
|
|
g.Expect(obj.Status.LastHandledReconcileAt).To(Equal("now"))
|
|
}, timeout).Should(Succeed())
|
|
// Nothing else should change.
|
|
g.Expect(obj.Status.ObservedSourceRevision).To(Equal(srcRevBefore))
|
|
g.Expect(obj.Status.LastPushCommit).To(Equal(pushCommitBefore))
|
|
g.Expect(obj.Status.LastPushTime).To(Equal(pushTimeBefore))
|
|
|
|
// Push a new commit such that there's no new update and
|
|
// reconcile again.
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Update setter marker", func(tmp string) {
|
|
marker := fmt.Sprintf(`{"$imagepolicy": "%s:%s"}`, policyKey1.Namespace, policyKey1.Name)
|
|
g.Expect(testutil.ReplaceMarkerWithMarker(filepath.Join(tmp, "deploy.yaml"), policyKey2, marker))
|
|
})
|
|
patch = client.MergeFrom(obj.DeepCopy())
|
|
obj.SetAnnotations(map[string]string{meta.ReconcileRequestAnnotation: "nownow"})
|
|
g.Expect(testEnv.Patch(ctx, &obj, patch)).To(Succeed())
|
|
|
|
// Look for the ObservedSourceRevision to update.
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(conditions.IsReady(&obj)).To(BeTrue())
|
|
g.Expect(obj.Status.ObservedSourceRevision).ToNot(Equal(srcRevBefore))
|
|
}, timeout).Should(Succeed())
|
|
observedPoliciesBefore := obj.Status.ObservedPolicies
|
|
srcRevBefore = obj.Status.ObservedSourceRevision
|
|
|
|
// Update the policy, there will be no new update due to the
|
|
// setter set above, reconcile again.
|
|
latest = "helloworld:v2.0.0"
|
|
g.Expect(updateImagePolicyWithLatestImage(ctx, testEnv, s.imagePolicyName, s.namespace, latest)).To(Succeed())
|
|
|
|
// Look for the ObservedPolicies to update.
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(conditions.IsReady(&obj)).To(BeTrue())
|
|
g.Expect(obj.Status.ObservedPolicies).ToNot(Equal(observedPoliciesBefore))
|
|
}, timeout).Should(Succeed())
|
|
g.Expect(obj.Status.ObservedSourceRevision).To(Equal(srcRevBefore))
|
|
})
|
|
})
|
|
|
|
t.Run("error recovery with early return", func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
namespace, err := testEnv.CreateNamespace(ctx, "test-update")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed())
|
|
}()
|
|
|
|
testWithRepoAndImagePolicy(ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
err := createImageUpdateAutomation(ctx, testEnv, updateName, s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, s.branch, "", testCommitTemplate, "", nil)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, updateName, s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
objKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: updateName,
|
|
}
|
|
var obj imagev1.ImageUpdateAutomation
|
|
|
|
// Ensure the image update is ready.
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(obj.Status.ObservedGeneration).To(Equal(obj.GetGeneration()))
|
|
g.Expect(conditions.IsReady(&obj))
|
|
}, timeout).Should(Succeed())
|
|
|
|
g.Expect(obj.Status.ObservedSourceRevision).ToNot(BeEmpty())
|
|
|
|
// Update the GitRepository to add a non-existing secret ref.
|
|
gitRepoKey := types.NamespacedName{
|
|
Namespace: s.gitRepoNamespace,
|
|
Name: s.gitRepoName,
|
|
}
|
|
var gitRepo sourcev1.GitRepository
|
|
g.Expect(testEnv.Get(ctx, gitRepoKey, &gitRepo)).To(Succeed())
|
|
patch := client.MergeFrom(gitRepo.DeepCopy())
|
|
gitRepo.Spec.SecretRef = &meta.LocalObjectReference{Name: "non-existing-git-sec"}
|
|
g.Expect(testEnv.Patch(ctx, &gitRepo, patch)).To(Succeed())
|
|
|
|
// Wait for image update to fail.
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(conditions.IsReady(&obj)).To(BeFalse())
|
|
}, timeout).Should(Succeed())
|
|
|
|
// Patch the GitRepository to remove the secret ref.
|
|
patch = client.MergeFrom(gitRepo.DeepCopy())
|
|
gitRepo.Spec.SecretRef = nil
|
|
g.Expect(testEnv.Patch(ctx, &gitRepo, patch)).To(Succeed())
|
|
|
|
// Wait for image update to recover from failure.
|
|
g.Eventually(func(g Gomega) {
|
|
g.Expect(testEnv.Get(ctx, objKey, &obj)).To(Succeed())
|
|
g.Expect(conditions.IsReady(&obj)).To(BeTrue())
|
|
}, timeout).Should(Succeed())
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_commitMessage(t *testing.T) {
|
|
policySpec := imagev1_reflect.ImagePolicySpec{
|
|
ImageRepositoryRef: meta.NamespacedObjectReference{
|
|
Name: "not-expected-to-exist",
|
|
},
|
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
|
Range: "1.x",
|
|
},
|
|
},
|
|
}
|
|
fixture := "testdata/appconfig"
|
|
latest := "helloworld:v1.0.0"
|
|
|
|
tests := []struct {
|
|
name string
|
|
template string
|
|
wantCommitMsg func(policyName, policyNS string) string
|
|
}{
|
|
{
|
|
name: "template with update Result",
|
|
template: testCommitTemplate,
|
|
wantCommitMsg: func(policyName, policyNS string) string {
|
|
return fmt.Sprintf(testCommitMessageFmt, policyNS, policyName)
|
|
},
|
|
},
|
|
{
|
|
name: "template with update ResultV2",
|
|
template: `Commit summary with ResultV2
|
|
|
|
Automation: {{ .AutomationObject }}
|
|
|
|
{{ range $filename, $objchange := .Changed.FileChanges -}}
|
|
- File: {{ $filename }}
|
|
{{- range $obj, $changes := $objchange }}
|
|
- Object: {{ $obj.Kind }}/{{ $obj.Namespace }}/{{ $obj.Name }}
|
|
Changes:
|
|
{{- range $_ , $change := $changes }}
|
|
- {{ $change.OldValue }} -> {{ $change.NewValue }} ({{ $change.Setter }})
|
|
{{ end -}}
|
|
{{ end -}}
|
|
{{ end -}}
|
|
`,
|
|
wantCommitMsg: func(policyName, policyNS string) string {
|
|
return fmt.Sprintf(`Commit summary with ResultV2
|
|
|
|
Automation: %s/update-test
|
|
|
|
- File: deploy.yaml
|
|
- Object: Deployment//test
|
|
Changes:
|
|
- helloworld:1.0.0 -> helloworld:v1.0.0 (%s:%s)
|
|
`, policyNS, policyNS, policyName)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Create test namespace.
|
|
namespace, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(
|
|
ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
commitMessage := tt.wantCommitMsg(s.imagePolicyName, s.namespace)
|
|
|
|
// Update the setter marker in the repo.
|
|
policyKey := types.NamespacedName{
|
|
Name: s.imagePolicyName,
|
|
Namespace: s.namespace,
|
|
}
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
// Pull the head commit we just pushed, so it's not
|
|
// considered a new commit when checking for a commit
|
|
// made by automation.
|
|
preChangeCommitId := testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
// Pull the head commit that was just pushed, so it's not considered a new
|
|
// commit when checking for a commit made by automation.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
preChangeCommitId = testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
// Create the automation object and let it make a commit itself.
|
|
updateStrategy := &imagev1.UpdateStrategy{
|
|
Strategy: imagev1.UpdateStrategySetters,
|
|
}
|
|
err := createImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, s.branch, "", tt.template, "", updateStrategy)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
// Wait for a new commit to be made by the controller.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
head, _ := localRepo.Head()
|
|
commit, err := localRepo.CommitObject(head.Hash())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(commit.Message).To(Equal(commitMessage))
|
|
|
|
signature := commit.Author
|
|
g.Expect(signature).NotTo(BeNil())
|
|
g.Expect(signature.Name).To(Equal(testAuthorName))
|
|
g.Expect(signature.Email).To(Equal(testAuthorEmail))
|
|
|
|
// Regression check to ensure the status message contains the branch name
|
|
// if checkout branch is the same as push branch.
|
|
imageUpdateKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: "update-test",
|
|
}
|
|
var imageUpdate imagev1.ImageUpdateAutomation
|
|
_ = testEnv.Get(context.TODO(), imageUpdateKey, &imageUpdate)
|
|
ready := apimeta.FindStatusCondition(imageUpdate.Status.Conditions, meta.ReadyCondition)
|
|
g.Expect(ready.Message).To(Equal(readyMessage))
|
|
g.Expect(imageUpdate.Status.LastPushCommit).To(Equal(head.Hash().String()))
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, &imageUpdate)
|
|
},
|
|
)
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_crossNamespaceRef(t *testing.T) {
|
|
g := NewWithT(t)
|
|
policySpec := imagev1_reflect.ImagePolicySpec{
|
|
ImageRepositoryRef: meta.NamespacedObjectReference{
|
|
Name: "not-expected-to-exist",
|
|
},
|
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
|
Range: "1.x",
|
|
},
|
|
},
|
|
}
|
|
fixture := "testdata/appconfig"
|
|
latest := "helloworld:v1.0.0"
|
|
|
|
// Test successful cross namespace reference when NoCrossNamespaceRef=false.
|
|
|
|
// Create test namespace.
|
|
namespace1, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace1)).To(Succeed()) }()
|
|
|
|
args := newRepoAndPolicyArgs(namespace1.Name)
|
|
|
|
// Create another test namespace.
|
|
namespace2, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace2)).To(Succeed()) }()
|
|
|
|
args.gitRepoNamespace = namespace2.Name
|
|
|
|
testWithCustomRepoAndImagePolicy(
|
|
ctx, g, testEnv, fixture, policySpec, latest, args,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
commitMessage := fmt.Sprintf(testCommitMessageFmt, s.namespace, s.imagePolicyName)
|
|
|
|
// Update the setter marker in the repo.
|
|
policyKey := types.NamespacedName{
|
|
Name: s.imagePolicyName,
|
|
Namespace: s.namespace,
|
|
}
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
// Pull the head commit we just pushed, so it's not
|
|
// considered a new commit when checking for a commit
|
|
// made by automation.
|
|
preChangeCommitId := testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
// Pull the head commit that was just pushed, so it's not considered a new
|
|
// commit when checking for a commit made by automation.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
preChangeCommitId = testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
// Create the automation object and let it make a commit itself.
|
|
updateStrategy := &imagev1.UpdateStrategy{
|
|
Strategy: imagev1.UpdateStrategySetters,
|
|
}
|
|
err := createImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, "", updateStrategy)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
// Wait for a new commit to be made by the controller.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
head, _ := localRepo.Head()
|
|
commit, err := localRepo.CommitObject(head.Hash())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(commit.Message).To(Equal(commitMessage))
|
|
|
|
signature := commit.Author
|
|
g.Expect(signature).NotTo(BeNil())
|
|
g.Expect(signature.Name).To(Equal(testAuthorName))
|
|
g.Expect(signature.Email).To(Equal(testAuthorEmail))
|
|
},
|
|
)
|
|
|
|
// Test cross namespace reference failure when NoCrossNamespaceRef=true.
|
|
r := &ImageUpdateAutomationReconciler{
|
|
Client: fakeclient.NewClientBuilder().
|
|
WithScheme(testEnv.Scheme()).
|
|
WithStatusSubresource(&imagev1.ImageUpdateAutomation{}, &imagev1_reflect.ImagePolicy{}).
|
|
Build(),
|
|
EventRecorder: testEnv.GetEventRecorderFor("image-automation-controller"),
|
|
NoCrossNamespaceRef: true,
|
|
}
|
|
|
|
// Create test namespace.
|
|
namespace3, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace3)).To(Succeed()) }()
|
|
|
|
// Test successful cross namespace reference when NoCrossNamespaceRef=false.
|
|
args = newRepoAndPolicyArgs(namespace3.Name)
|
|
|
|
// Create another test namespace.
|
|
namespace4, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace4)).To(Succeed()) }()
|
|
|
|
args.gitRepoNamespace = namespace4.Name
|
|
|
|
testWithCustomRepoAndImagePolicy(
|
|
ctx, g, r.Client, fixture, policySpec, latest, args,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
updateStrategy := &imagev1.UpdateStrategy{
|
|
Strategy: imagev1.UpdateStrategySetters,
|
|
}
|
|
err := createImageUpdateAutomation(ctx, r.Client, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, "", updateStrategy)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
imageUpdateKey := types.NamespacedName{
|
|
Name: "update-test",
|
|
Namespace: s.namespace,
|
|
}
|
|
var imageUpdate imagev1.ImageUpdateAutomation
|
|
_ = r.Client.Get(context.TODO(), imageUpdateKey, &imageUpdate)
|
|
|
|
sp := patch.NewSerialPatcher(&imageUpdate, r.Client)
|
|
|
|
_, err = r.reconcile(context.TODO(), sp, &imageUpdate, time.Now())
|
|
g.Expect(err).To(BeNil())
|
|
|
|
ready := apimeta.FindStatusCondition(imageUpdate.Status.Conditions, meta.ReadyCondition)
|
|
g.Expect(ready.Reason).To(Equal(aclapi.AccessDeniedReason))
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_updatePath(t *testing.T) {
|
|
policySpec := imagev1_reflect.ImagePolicySpec{
|
|
ImageRepositoryRef: meta.NamespacedObjectReference{
|
|
Name: "not-expected-to-exist",
|
|
},
|
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
|
Range: "1.x",
|
|
},
|
|
},
|
|
}
|
|
fixture := "testdata/pathconfig"
|
|
latest := "helloworld:v1.0.0"
|
|
|
|
g := NewWithT(t)
|
|
|
|
// Create test namespace.
|
|
namespace, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(
|
|
ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
// Update the setter marker in the repo.
|
|
policyKey := types.NamespacedName{
|
|
Name: s.imagePolicyName,
|
|
Namespace: s.namespace,
|
|
}
|
|
|
|
// pull the head commit we just pushed, so it's not
|
|
// considered a new commit when checking for a commit
|
|
// made by automation.
|
|
preChangeCommitId := testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "yes", "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "no", "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
// Pull the head commit that was just pushed, so it's not considered a new
|
|
// commit when checking for a commit made by automation.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
preChangeCommitId = testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
// Create the automation object and let it make a commit itself.
|
|
updateStrategy := &imagev1.UpdateStrategy{
|
|
Strategy: imagev1.UpdateStrategySetters,
|
|
Path: "./yes",
|
|
}
|
|
err := createImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, "", updateStrategy)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
// Wait for a new commit to be made by the controller.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
head, _ := localRepo.Head()
|
|
commit, err := localRepo.CommitObject(head.Hash())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(commit.Message).ToNot(ContainSubstring("update-no"))
|
|
g.Expect(commit.Message).To(ContainSubstring("update-yes"))
|
|
|
|
var update imagev1.ImageUpdateAutomation
|
|
updateKey := types.NamespacedName{
|
|
Namespace: s.namespace,
|
|
Name: "update-test",
|
|
}
|
|
g.Expect(testEnv.Get(ctx, updateKey, &update)).To(Succeed())
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, &update)
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_signedCommit(t *testing.T) {
|
|
policySpec := imagev1_reflect.ImagePolicySpec{
|
|
ImageRepositoryRef: meta.NamespacedObjectReference{
|
|
Name: "not-expected-to-exist",
|
|
},
|
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
|
Range: "1.x",
|
|
},
|
|
},
|
|
}
|
|
fixture := "testdata/appconfig"
|
|
latest := "helloworld:v1.0.0"
|
|
|
|
g := NewWithT(t)
|
|
|
|
// Create test namespace.
|
|
namespace, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
testWithRepoAndImagePolicy(
|
|
ctx, g, testEnv, namespace.Name, fixture, policySpec, latest,
|
|
func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository) {
|
|
signingKeySecretName := "signing-key-secret-" + rand.String(5)
|
|
// Update the setter marker in the repo.
|
|
policyKey := types.NamespacedName{
|
|
Name: s.imagePolicyName,
|
|
Namespace: s.namespace,
|
|
}
|
|
_ = testutil.CommitInRepo(ctx, g, repoURL, s.branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
preChangeCommitId := testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
// Pull the head commit that was just pushed, so it's not considered a new
|
|
// commit when checking for a commit made by automation.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
pgpEntity := createSigningKeyPairSecret(ctx, g, testEnv, signingKeySecretName, s.namespace)
|
|
|
|
preChangeCommitId = testutil.CommitIdFromBranch(localRepo, s.branch)
|
|
|
|
// Create the automation object and let it make a commit itself.
|
|
updateStrategy := &imagev1.UpdateStrategy{
|
|
Strategy: imagev1.UpdateStrategySetters,
|
|
}
|
|
err := createImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", "", testCommitTemplate, signingKeySecretName, updateStrategy)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, "update-test", s.namespace)).To(Succeed())
|
|
}()
|
|
|
|
// Wait for a new commit to be made by the controller.
|
|
waitForNewHead(g, localRepo, s.branch, preChangeCommitId)
|
|
|
|
head, _ := localRepo.Head()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
commit, err := localRepo.CommitObject(head.Hash())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
c2 := *commit
|
|
c2.PGPSignature = ""
|
|
|
|
encoded := &plumbing.MemoryObject{}
|
|
err = c2.Encode(encoded)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
content, err := encoded.Reader()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
kr := openpgp.EntityList([]*openpgp.Entity{pgpEntity})
|
|
signature := strings.NewReader(commit.PGPSignature)
|
|
|
|
_, err = openpgp.CheckArmoredDetachedSignature(kr, content, signature, nil)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
},
|
|
)
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_e2e(t *testing.T) {
|
|
protos := []string{"http", "ssh"}
|
|
|
|
testFunc := func(t *testing.T, proto string) {
|
|
g := NewWithT(t)
|
|
|
|
const latestImage = "helloworld:1.0.1"
|
|
|
|
// Create a test namespace.
|
|
namespace, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
branch := rand.String(8)
|
|
repositoryPath := "/config-" + rand.String(6) + ".git"
|
|
gitRepoName := "image-auto-" + rand.String(5)
|
|
gitSecretName := "git-secret-" + rand.String(5)
|
|
imagePolicyName := "policy-" + rand.String(5)
|
|
updateStrategy := &imagev1.UpdateStrategy{
|
|
Strategy: imagev1.UpdateStrategySetters,
|
|
}
|
|
|
|
// Create git server.
|
|
gitServer := testutil.SetUpGitTestServer(g)
|
|
defer os.RemoveAll(gitServer.Root())
|
|
defer gitServer.StopHTTP()
|
|
|
|
cloneLocalRepoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath
|
|
repoURL, err := getRepoURL(gitServer, repositoryPath, proto)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Start the ssh server if needed.
|
|
if proto == "ssh" {
|
|
go func() {
|
|
gitServer.StartSSH()
|
|
}()
|
|
defer func() {
|
|
g.Expect(gitServer.StopSSH()).To(Succeed())
|
|
}()
|
|
}
|
|
|
|
commitMessage := "Commit a difference " + rand.String(5)
|
|
|
|
// Initialize a git repo.
|
|
_ = testutil.InitGitRepo(g, gitServer, "testdata/appconfig", branch, repositoryPath)
|
|
|
|
// Create GitRepository resource for the above repo.
|
|
if proto == "ssh" {
|
|
// SSH requires an identity (private key) and known_hosts file
|
|
// in a secret.
|
|
err = createSSHIdentitySecret(testEnv, gitSecretName, namespace.Name, repoURL)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
err = createGitRepository(ctx, testEnv, gitRepoName, namespace.Name, repoURL, gitSecretName)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
} else {
|
|
err = createGitRepository(ctx, testEnv, gitRepoName, namespace.Name, repoURL, "")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
}
|
|
|
|
// Create an image policy.
|
|
policyKey := types.NamespacedName{
|
|
Name: imagePolicyName,
|
|
Namespace: namespace.Name,
|
|
}
|
|
|
|
// Clone the repo locally.
|
|
cloneCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
localRepo, cloneDir, err := testutil.Clone(cloneCtx, cloneLocalRepoURL, branch, originRemote)
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to clone")
|
|
defer func() { os.RemoveAll(cloneDir) }()
|
|
|
|
testutil.CheckoutBranch(g, localRepo, branch)
|
|
err = createImagePolicyWithLatestImage(ctx, testEnv, imagePolicyName, namespace.Name, "not-expected-to-exist", "1.x", latestImage)
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to create ImagePolicy resource")
|
|
|
|
defer func() {
|
|
g.Expect(deleteImagePolicy(ctx, testEnv, imagePolicyName, namespace.Name)).ToNot(HaveOccurred())
|
|
}()
|
|
|
|
preChangeCommitId := testutil.CommitIdFromBranch(localRepo, branch)
|
|
// Insert a setter reference into the deployment file,
|
|
// before creating the automation object itself.
|
|
_ = testutil.CommitInRepo(ctx, g, cloneLocalRepoURL, branch, originRemote, "Install setter marker", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
// Pull the head commit we just pushed, so it's not
|
|
// considered a new commit when checking for a commit
|
|
// made by automation.
|
|
waitForNewHead(g, localRepo, branch, preChangeCommitId)
|
|
|
|
preChangeCommitId = testutil.CommitIdFromBranch(localRepo, branch)
|
|
|
|
// Now create the automation object, and let it make a commit itself.
|
|
updateKey := types.NamespacedName{
|
|
Namespace: namespace.Name,
|
|
Name: "update-" + rand.String(5),
|
|
}
|
|
err = createImageUpdateAutomation(ctx, testEnv, updateKey.Name, namespace.Name, gitRepoName, namespace.Name, branch, "", "", commitMessage, "", updateStrategy)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() {
|
|
g.Expect(deleteImageUpdateAutomation(ctx, testEnv, updateKey.Name, namespace.Name)).To(Succeed())
|
|
}()
|
|
|
|
var imageUpdate imagev1.ImageUpdateAutomation
|
|
g.Eventually(func() bool {
|
|
if err := testEnv.Get(ctx, updateKey, &imageUpdate); err != nil {
|
|
return false
|
|
}
|
|
return conditions.IsReady(&imageUpdate) && imageUpdate.Status.LastPushCommit != ""
|
|
}, timeout).Should(BeTrue())
|
|
|
|
// Wait for a new commit to be made by the controller.
|
|
waitForNewHead(g, localRepo, branch, preChangeCommitId)
|
|
|
|
// Check if the repo head matches with the ImageUpdateAutomation
|
|
// last push commit status.
|
|
head, _ := localRepo.Head()
|
|
commit, err := localRepo.CommitObject(head.Hash())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
g.Expect(commit.Message).To(Equal(commitMessage))
|
|
g.Expect(commit.Hash.String()).To(Equal(imageUpdate.Status.LastPushCommit))
|
|
|
|
checkCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
compareRepoWithExpected(checkCtx, g, cloneLocalRepoURL, branch, "testdata/appconfig-setters-expected", func(tmp string) {
|
|
g.Expect(testutil.ReplaceMarker(filepath.Join(tmp, "deploy.yaml"), policyKey)).To(Succeed())
|
|
})
|
|
|
|
// Check if the object status is valid.
|
|
condns := &conditionscheck.Conditions{NegativePolarity: imageUpdateAutomationNegativeConditions}
|
|
checker := conditionscheck.NewChecker(testEnv.Client, condns)
|
|
checker.WithT(g).CheckErr(ctx, &imageUpdate)
|
|
}
|
|
|
|
for _, proto := range protos {
|
|
t.Run(proto, func(t *testing.T) {
|
|
testFunc(t, proto)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_defaulting(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
branch := rand.String(8)
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
// Create a test namespace.
|
|
namespace, err := testEnv.CreateNamespace(ctx, "image-auto-test")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer func() { g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) }()
|
|
|
|
// Create an instance of ImageUpdateAutomation.
|
|
key := types.NamespacedName{
|
|
Name: "update-" + rand.String(5),
|
|
Namespace: namespace.Name,
|
|
}
|
|
auto := &imagev1.ImageUpdateAutomation{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: key.Name,
|
|
Namespace: key.Namespace,
|
|
},
|
|
Spec: imagev1.ImageUpdateAutomationSpec{
|
|
SourceRef: imagev1.CrossNamespaceSourceReference{
|
|
Kind: "GitRepository",
|
|
Name: "garbage",
|
|
},
|
|
Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing
|
|
GitSpec: &imagev1.GitSpec{
|
|
Checkout: &imagev1.GitCheckoutSpec{
|
|
Reference: sourcev1.GitRepositoryRef{
|
|
Branch: branch,
|
|
},
|
|
},
|
|
// leave Update field out
|
|
Commit: imagev1.CommitSpec{
|
|
Author: imagev1.CommitUser{
|
|
Email: testAuthorEmail,
|
|
},
|
|
MessageTemplate: "nothing",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
g.Expect(testEnv.Create(ctx, auto)).To(Succeed())
|
|
defer func() {
|
|
g.Expect(testEnv.Delete(ctx, auto)).To(Succeed())
|
|
}()
|
|
|
|
// Should default .spec.update to {strategy: Setters}.
|
|
var fetchedAuto imagev1.ImageUpdateAutomation
|
|
g.Eventually(func() bool {
|
|
err := testEnv.Get(ctx, key, &fetchedAuto)
|
|
return err == nil
|
|
}, timeout, time.Second).Should(BeTrue())
|
|
g.Expect(fetchedAuto.Spec.Update).
|
|
To(Equal(&imagev1.UpdateStrategy{Strategy: imagev1.UpdateStrategySetters}))
|
|
}
|
|
|
|
func TestImageUpdateAutomationReconciler_notify(t *testing.T) {
|
|
g := NewWithT(t)
|
|
testPushResult, err := source.NewPushResult("branch", "rev", "test commit message")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
tests := []struct {
|
|
name string
|
|
pushResult *source.PushResult
|
|
syncNeeded bool
|
|
oldObjBeforeFunc func(obj conditions.Setter)
|
|
newObjBeforeFunc func(obj conditions.Setter)
|
|
wantEvent string
|
|
}{
|
|
{
|
|
name: "first time reconciliation, no update",
|
|
pushResult: nil,
|
|
syncNeeded: true,
|
|
newObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
wantEvent: "Normal Succeeded repository up-to-date",
|
|
},
|
|
{
|
|
name: "second reconciliation, syncNeeded=false, no update",
|
|
pushResult: nil,
|
|
syncNeeded: false,
|
|
oldObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
newObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
wantEvent: "Trace Succeeded no change since last reconciliation",
|
|
},
|
|
{
|
|
name: "second reconciliation, syncNeeded=true, no update",
|
|
pushResult: nil,
|
|
syncNeeded: true,
|
|
oldObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
newObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
wantEvent: "Trace Succeeded repository up-to-date",
|
|
},
|
|
{
|
|
name: "was ready, new update, is ready",
|
|
pushResult: testPushResult,
|
|
syncNeeded: true,
|
|
oldObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
newObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
wantEvent: "Normal Succeeded pushed commit 'rev' to branch 'branch'\ntest commit message",
|
|
},
|
|
{
|
|
name: "failure recovery, no update",
|
|
pushResult: nil,
|
|
syncNeeded: true,
|
|
oldObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, "failed to checkout source")
|
|
},
|
|
newObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
wantEvent: "Normal Succeeded repository up-to-date",
|
|
},
|
|
{
|
|
name: "failure recovery, with new update",
|
|
pushResult: testPushResult,
|
|
syncNeeded: true,
|
|
oldObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, "failed to checkout source")
|
|
},
|
|
newObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
wantEvent: "Normal Succeeded pushed commit 'rev' to branch 'branch'\ntest commit message",
|
|
},
|
|
{
|
|
name: "failed",
|
|
pushResult: nil,
|
|
syncNeeded: true,
|
|
oldObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkTrue(obj, meta.ReadyCondition, meta.SucceededReason, "%s", readyMessage)
|
|
},
|
|
newObjBeforeFunc: func(obj conditions.Setter) {
|
|
conditions.MarkFalse(obj, meta.ReadyCondition, imagev1.GitOperationFailedReason, "failed to checkout source")
|
|
},
|
|
wantEvent: "Warning GitOperationFailed failed to checkout source",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
recorder := record.NewFakeRecorder(32)
|
|
|
|
oldObj := &imagev1.ImageUpdateAutomation{}
|
|
newObj := oldObj.DeepCopy()
|
|
|
|
if tt.oldObjBeforeFunc != nil {
|
|
tt.oldObjBeforeFunc(oldObj)
|
|
}
|
|
if tt.newObjBeforeFunc != nil {
|
|
tt.newObjBeforeFunc(newObj)
|
|
}
|
|
|
|
reconciler := &ImageUpdateAutomationReconciler{
|
|
EventRecorder: recorder,
|
|
}
|
|
reconciler.notify(ctx, oldObj, newObj, tt.pushResult, tt.syncNeeded)
|
|
|
|
select {
|
|
case x, ok := <-recorder.Events:
|
|
g.Expect(ok).To(Equal(tt.wantEvent != ""), "unexpected event received")
|
|
if tt.wantEvent != "" {
|
|
g.Expect(x).To(ContainSubstring(tt.wantEvent))
|
|
}
|
|
default:
|
|
if tt.wantEvent != "" {
|
|
g.Fail("expected some event to be emitted")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getPolicies(t *testing.T) {
|
|
testNS1 := "foo"
|
|
testNS2 := "bar"
|
|
|
|
type policyArgs struct {
|
|
name string
|
|
namespace string
|
|
latestImage string
|
|
labels map[string]string
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
listNamespace string
|
|
selector *metav1.LabelSelector
|
|
policies []policyArgs
|
|
wantPolicies []string
|
|
}{
|
|
{
|
|
name: "lists policies with image and in same namespace",
|
|
listNamespace: testNS1,
|
|
policies: []policyArgs{
|
|
{name: "p1", namespace: testNS1, latestImage: "aaa:bbb"},
|
|
{name: "p2", namespace: testNS1, latestImage: "ccc:ddd"},
|
|
{name: "p3", namespace: testNS2, latestImage: "eee:fff"},
|
|
{name: "p4", namespace: testNS1, latestImage: ""},
|
|
},
|
|
wantPolicies: []string{"p1", "p2"},
|
|
},
|
|
{
|
|
name: "lists policies with label selector in same namespace",
|
|
listNamespace: testNS1,
|
|
selector: &metav1.LabelSelector{
|
|
MatchLabels: map[string]string{
|
|
"label": "one",
|
|
},
|
|
},
|
|
policies: []policyArgs{
|
|
{name: "p1", namespace: testNS1, latestImage: "aaa:bbb", labels: map[string]string{"label": "one"}},
|
|
{name: "p2", namespace: testNS1, latestImage: "ccc:ddd", labels: map[string]string{"label": "false"}},
|
|
{name: "p3", namespace: testNS2, latestImage: "eee:fff", labels: map[string]string{"label": "one"}},
|
|
},
|
|
wantPolicies: []string{"p1"},
|
|
},
|
|
{
|
|
name: "no policies in empty namespace",
|
|
listNamespace: testNS2,
|
|
policies: []policyArgs{
|
|
{name: "p1", namespace: testNS1, latestImage: "aaa:bbb"},
|
|
},
|
|
wantPolicies: []string{},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Create all the test policies.
|
|
testObjects := []client.Object{}
|
|
for _, p := range tt.policies {
|
|
aPolicy := &imagev1_reflect.ImagePolicy{}
|
|
aPolicy.Name = p.name
|
|
aPolicy.Namespace = p.namespace
|
|
aPolicy.Status = imagev1_reflect.ImagePolicyStatus{
|
|
LatestRef: testutil.ImageToRef(p.latestImage),
|
|
}
|
|
aPolicy.Labels = p.labels
|
|
testObjects = append(testObjects, aPolicy)
|
|
}
|
|
kClient := fakeclient.NewClientBuilder().
|
|
WithScheme(testEnv.GetScheme()).
|
|
WithObjects(testObjects...).Build()
|
|
|
|
result, err := getPolicies(context.TODO(), kClient, tt.listNamespace, tt.selector)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Extract policy name from the result and compare with the expected
|
|
// result.
|
|
resultPolicyNames := []string{}
|
|
for _, r := range result {
|
|
resultPolicyNames = append(resultPolicyNames, r.Name)
|
|
}
|
|
g.Expect(resultPolicyNames).To(ContainElements(tt.wantPolicies))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_observedPoliciesChanged(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
previous imagev1.ObservedPolicies
|
|
current imagev1.ObservedPolicies
|
|
want bool
|
|
}{
|
|
{
|
|
name: "no change",
|
|
previous: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p2": imagev1.ImageRef{Name: "ccc", Tag: "ddd"},
|
|
},
|
|
current: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p2": imagev1.ImageRef{Name: "ccc", Tag: "ddd"},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "change due to new tag",
|
|
previous: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p2": imagev1.ImageRef{Name: "ccc", Tag: "ddd"},
|
|
},
|
|
current: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p2": imagev1.ImageRef{Name: "ccc", Tag: "zzz"},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "change due to different policies, same count",
|
|
previous: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p2": imagev1.ImageRef{Name: "ccc", Tag: "ddd"},
|
|
},
|
|
current: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p3": imagev1.ImageRef{Name: "ccc", Tag: "ddd"},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "change due to new policy, different count",
|
|
previous: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
},
|
|
current: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p2": imagev1.ImageRef{Name: "ccc", Tag: "ddd"},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "change due to deleted policy",
|
|
previous: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
"p2": imagev1.ImageRef{Name: "ccc", Tag: "ddd"},
|
|
},
|
|
current: imagev1.ObservedPolicies{
|
|
"p1": imagev1.ImageRef{Name: "aaa", Tag: "bbb"},
|
|
},
|
|
want: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
result := observedPoliciesChanged(tt.previous, tt.current)
|
|
g.Expect(result).To(Equal(tt.want))
|
|
})
|
|
}
|
|
}
|
|
|
|
func compareRepoWithExpected(ctx context.Context, g *WithT, repoURL, branch, fixture string, changeFixture func(tmp string)) {
|
|
g.THelper()
|
|
|
|
expected, err := os.MkdirTemp("", "gotest-imageauto-expected")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
defer os.RemoveAll(expected)
|
|
|
|
copy.Copy(fixture, expected)
|
|
changeFixture(expected)
|
|
repo, cloneDir, err := testutil.Clone(ctx, repoURL, branch, originRemote)
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to clone")
|
|
defer func() { os.RemoveAll(cloneDir) }()
|
|
|
|
// NOTE: The workdir contains a trailing /. Clean it to not confuse the
|
|
// DiffDirectories().
|
|
wt, err := repo.Worktree()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
defer wt.Filesystem.Remove(".")
|
|
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
test.ExpectMatchingDirectories(g, wt.Filesystem.Root(), expected)
|
|
}
|
|
|
|
func waitForNewHead(g *WithT, repo *extgogit.Repository, branch, preChangeHash string) {
|
|
g.THelper()
|
|
|
|
var commitToResetTo *object.Commit
|
|
|
|
origin, err := repo.Remote(originRemote)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Now try to fetch new commits from that remote branch
|
|
g.Eventually(func() bool {
|
|
err := origin.Fetch(&extgogit.FetchOptions{
|
|
RemoteName: originRemote,
|
|
RefSpecs: []config.RefSpec{config.RefSpec(testutil.BranchRefName(branch))},
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
wt, err := repo.Worktree()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
err = wt.Checkout(&extgogit.CheckoutOptions{
|
|
Branch: plumbing.NewBranchReferenceName(branch),
|
|
})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
remoteHeadRef, err := repo.Head()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
remoteHeadHash := remoteHeadRef.Hash()
|
|
|
|
if preChangeHash != remoteHeadHash.String() {
|
|
commitToResetTo, _ = repo.CommitObject(remoteHeadHash)
|
|
return true
|
|
}
|
|
return false
|
|
}, timeout, time.Second).Should(BeTrue())
|
|
|
|
if commitToResetTo != nil {
|
|
wt, err := repo.Worktree()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// New commits in the remote branch -- reset the working tree head
|
|
// to that. Note this does not create a local branch tracking the
|
|
// remote, so it is a detached head.
|
|
g.Expect(wt.Reset(&extgogit.ResetOptions{
|
|
Commit: commitToResetTo.Hash,
|
|
Mode: extgogit.HardReset,
|
|
})).To(Succeed())
|
|
}
|
|
}
|
|
|
|
type repoAndPolicyArgs struct {
|
|
namespace, imagePolicyName, gitRepoName, branch, gitRepoNamespace string
|
|
}
|
|
|
|
// newRepoAndPolicyArgs generates random git repo, branch and image
|
|
// policy names to be used in the test. The gitRepoNamespace is set the same
|
|
// as the overall given namespace. For different git repo namespace, the caller
|
|
// may assign it as per the needs.
|
|
func newRepoAndPolicyArgs(namespace string) repoAndPolicyArgs {
|
|
args := repoAndPolicyArgs{
|
|
namespace: namespace,
|
|
gitRepoName: "image-auto-test-" + rand.String(5),
|
|
gitRepoNamespace: namespace,
|
|
branch: rand.String(8),
|
|
imagePolicyName: "policy-" + rand.String(5),
|
|
}
|
|
return args
|
|
}
|
|
|
|
// testWithRepoAndImagePolicyTestFunc is the test closure function type passed
|
|
// to testWithRepoAndImagePolicy.
|
|
type testWithRepoAndImagePolicyTestFunc func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *extgogit.Repository)
|
|
|
|
// testWithRepoAndImagePolicy generates a repoAndPolicyArgs with all the
|
|
// resource in the given namespace and runs the given repo and image policy test.
|
|
func testWithRepoAndImagePolicy(
|
|
ctx context.Context,
|
|
g *WithT,
|
|
kClient client.Client,
|
|
namespace string,
|
|
fixture string,
|
|
policySpec imagev1_reflect.ImagePolicySpec,
|
|
latest string,
|
|
testFunc testWithRepoAndImagePolicyTestFunc) {
|
|
// Generate unique repo and policy arguments.
|
|
args := newRepoAndPolicyArgs(namespace)
|
|
testWithCustomRepoAndImagePolicy(ctx, g, kClient, fixture, policySpec, latest, args, testFunc)
|
|
}
|
|
|
|
// testWithCustomRepoAndImagePolicy sets up a git server, a repository in the git
|
|
// server, a GitRepository object for the created git repo, and an ImagePolicy
|
|
// with the given policy spec based on a repoAndPolicyArgs. It calls testFunc
|
|
// to run the test in the created environment.
|
|
func testWithCustomRepoAndImagePolicy(
|
|
ctx context.Context,
|
|
g *WithT,
|
|
kClient client.Client,
|
|
fixture string,
|
|
policySpec imagev1_reflect.ImagePolicySpec,
|
|
latest string,
|
|
args repoAndPolicyArgs,
|
|
testFunc testWithRepoAndImagePolicyTestFunc) {
|
|
repositoryPath := "/config-" + rand.String(6) + ".git"
|
|
|
|
// Create test git server.
|
|
gitServer := testutil.SetUpGitTestServer(g)
|
|
defer os.RemoveAll(gitServer.Root())
|
|
defer gitServer.StopHTTP()
|
|
|
|
// Create a git repo.
|
|
_ = testutil.InitGitRepo(g, gitServer, fixture, args.branch, repositoryPath)
|
|
|
|
// Clone the repo.
|
|
repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath
|
|
cloneCtx, cancel := context.WithTimeout(ctx, timeout)
|
|
defer cancel()
|
|
localRepo, cloneDir, err := testutil.Clone(cloneCtx, repoURL, args.branch, originRemote)
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to clone")
|
|
defer func() { os.RemoveAll(cloneDir) }()
|
|
|
|
err = localRepo.DeleteRemote(originRemote)
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to delete existing remote origin")
|
|
localRepo.CreateRemote(&config.RemoteConfig{
|
|
Name: originRemote,
|
|
URLs: []string{repoURL},
|
|
})
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to create new remote origin")
|
|
|
|
// Create GitRepository resource for the above repo.
|
|
err = createGitRepository(ctx, kClient, args.gitRepoName, args.gitRepoNamespace, repoURL, "")
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to create GitRepository resource")
|
|
|
|
// Create ImagePolicy with populated latest image in the status.
|
|
err = createImagePolicyWithLatestImageForSpec(ctx, kClient, args.imagePolicyName, args.namespace, policySpec, latest)
|
|
g.Expect(err).ToNot(HaveOccurred(), "failed to create ImagePolicy resource")
|
|
|
|
testFunc(g, args, repoURL, localRepo)
|
|
}
|
|
|
|
func createGitRepository(ctx context.Context, kClient client.Client, name, namespace, repoURL, secretRef string) error {
|
|
gitRepo := &sourcev1.GitRepository{
|
|
Spec: sourcev1.GitRepositorySpec{
|
|
URL: repoURL,
|
|
Interval: metav1.Duration{Duration: time.Hour},
|
|
Timeout: &metav1.Duration{Duration: time.Minute},
|
|
},
|
|
}
|
|
gitRepo.Name = name
|
|
gitRepo.Namespace = namespace
|
|
if secretRef != "" {
|
|
gitRepo.Spec.SecretRef = &meta.LocalObjectReference{Name: secretRef}
|
|
}
|
|
return kClient.Create(ctx, gitRepo)
|
|
}
|
|
|
|
func createImagePolicyWithLatestImage(ctx context.Context, kClient client.Client, name, namespace, repoRef, semverRange, latest string) error {
|
|
policySpec := imagev1_reflect.ImagePolicySpec{
|
|
ImageRepositoryRef: meta.NamespacedObjectReference{
|
|
Name: repoRef,
|
|
},
|
|
Policy: imagev1_reflect.ImagePolicyChoice{
|
|
SemVer: &imagev1_reflect.SemVerPolicy{
|
|
Range: semverRange,
|
|
},
|
|
},
|
|
}
|
|
return createImagePolicyWithLatestImageForSpec(ctx, kClient, name, namespace, policySpec, latest)
|
|
}
|
|
|
|
func createImagePolicyWithLatestImageForSpec(ctx context.Context, kClient client.Client, name, namespace string, policySpec imagev1_reflect.ImagePolicySpec, latest string) error {
|
|
policy := &imagev1_reflect.ImagePolicy{
|
|
Spec: policySpec,
|
|
}
|
|
policy.Name = name
|
|
policy.Namespace = namespace
|
|
err := kClient.Create(ctx, policy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
patch := client.MergeFrom(policy.DeepCopy())
|
|
policy.Status.LatestRef = testutil.ImageToRef(latest)
|
|
return kClient.Status().Patch(ctx, policy, patch)
|
|
}
|
|
|
|
func updateImagePolicyWithLatestImage(ctx context.Context, kClient client.Client, name, namespace, latest string) error {
|
|
policy := &imagev1_reflect.ImagePolicy{}
|
|
key := types.NamespacedName{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
}
|
|
if err := kClient.Get(ctx, key, policy); err != nil {
|
|
return err
|
|
}
|
|
patch := client.MergeFrom(policy.DeepCopy())
|
|
policy.Status.LatestRef = testutil.ImageToRef(latest)
|
|
return kClient.Status().Patch(ctx, policy, patch)
|
|
}
|
|
|
|
func createImageUpdateAutomation(ctx context.Context, kClient client.Client, name, namespace,
|
|
gitRepo, gitRepoNamespace, checkoutBranch, pushBranch, pushRefspec, commitTemplate, signingKeyRef string,
|
|
updateStrategy *imagev1.UpdateStrategy) error {
|
|
updateAutomation := &imagev1.ImageUpdateAutomation{
|
|
Spec: imagev1.ImageUpdateAutomationSpec{
|
|
Interval: metav1.Duration{Duration: 2 * time.Hour}, // This is to ensure any subsequent run should be outside the scope of the testing.
|
|
SourceRef: imagev1.CrossNamespaceSourceReference{
|
|
Kind: "GitRepository",
|
|
Name: gitRepo,
|
|
Namespace: gitRepoNamespace,
|
|
},
|
|
GitSpec: &imagev1.GitSpec{
|
|
Checkout: &imagev1.GitCheckoutSpec{
|
|
Reference: sourcev1.GitRepositoryRef{
|
|
Branch: checkoutBranch,
|
|
},
|
|
},
|
|
Commit: imagev1.CommitSpec{
|
|
MessageTemplate: commitTemplate,
|
|
Author: imagev1.CommitUser{
|
|
Name: testAuthorName,
|
|
Email: testAuthorEmail,
|
|
},
|
|
},
|
|
},
|
|
Update: updateStrategy,
|
|
},
|
|
}
|
|
updateAutomation.Name = name
|
|
updateAutomation.Namespace = namespace
|
|
if pushRefspec != "" || pushBranch != "" {
|
|
updateAutomation.Spec.GitSpec.Push = &imagev1.PushSpec{
|
|
Refspec: pushRefspec,
|
|
Branch: pushBranch,
|
|
}
|
|
}
|
|
if signingKeyRef != "" {
|
|
updateAutomation.Spec.GitSpec.Commit.SigningKey = &imagev1.SigningKey{
|
|
SecretRef: meta.LocalObjectReference{Name: signingKeyRef},
|
|
}
|
|
}
|
|
return kClient.Create(ctx, updateAutomation)
|
|
}
|
|
|
|
func deleteImageUpdateAutomation(ctx context.Context, kClient client.Client, name, namespace string) error {
|
|
update := &imagev1.ImageUpdateAutomation{}
|
|
update.Name = name
|
|
update.Namespace = namespace
|
|
return kClient.Delete(ctx, update)
|
|
}
|
|
|
|
func deleteImagePolicy(ctx context.Context, kClient client.Client, name, namespace string) error {
|
|
imagePolicy := &imagev1_reflect.ImagePolicy{}
|
|
imagePolicy.Name = name
|
|
imagePolicy.Namespace = namespace
|
|
return kClient.Delete(ctx, imagePolicy)
|
|
}
|
|
|
|
func createSigningKeyPairSecret(ctx context.Context, g *WithT, kClient client.Client, name, namespace string) *openpgp.Entity {
|
|
secret, pgpEntity := testutil.GetSigningKeyPairSecret(g, name, namespace)
|
|
g.Expect(kClient.Create(ctx, secret)).To(Succeed())
|
|
return pgpEntity
|
|
}
|
|
|
|
func createSSHIdentitySecret(kClient client.Client, name, namespace, repoURL string) error {
|
|
url, err := url.Parse(repoURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
knownhosts, err := ssh.ScanHostKey(url.Host, 5*time.Second, []string{}, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
keygen := ssh.NewRSAGenerator(2048)
|
|
pair, err := keygen.Generate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sec := &corev1.Secret{
|
|
StringData: map[string]string{
|
|
"known_hosts": string(knownhosts),
|
|
"identity": string(pair.PrivateKey),
|
|
"identity.pub": string(pair.PublicKey),
|
|
},
|
|
// Without KAS, StringData and Data must be kept in sync manually.
|
|
Data: map[string][]byte{
|
|
"known_hosts": knownhosts,
|
|
"identity": pair.PrivateKey,
|
|
"identity.pub": pair.PublicKey,
|
|
},
|
|
}
|
|
sec.Name = name
|
|
sec.Namespace = namespace
|
|
return kClient.Create(ctx, sec)
|
|
}
|
|
|
|
func getRepoURL(gitServer *gittestserver.GitServer, repoPath, proto string) (string, error) {
|
|
if proto == "http" {
|
|
return gitServer.HTTPAddressWithCredentials() + repoPath, nil
|
|
} else if proto == "ssh" {
|
|
// This is expected to use 127.0.0.1, but host key
|
|
// checking usually wants a hostname, so use
|
|
// "localhost".
|
|
sshURL := strings.Replace(gitServer.SSHAddress(), "127.0.0.1", "localhost", 1)
|
|
return sshURL + repoPath, nil
|
|
}
|
|
return "", fmt.Errorf("proto not set to http or ssh")
|
|
}
|