diff --git a/.github/workflows/image-reuse.yaml b/.github/workflows/image-reuse.yaml index 17d77e86d5..970080e845 100644 --- a/.github/workflows/image-reuse.yaml +++ b/.github/workflows/image-reuse.yaml @@ -79,7 +79,7 @@ jobs: cosign-release: 'v2.2.0' - uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - - uses: docker/setup-buildx-action@0d103c3126aa41d772a8362f6aa67afac040f80c # v3.1.0 + - uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3.2.0 - name: Setup tags for container image as a CSV type run: | @@ -106,7 +106,7 @@ jobs: echo 'EOF' >> $GITHUB_ENV - name: Login to Quay.io - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 with: registry: quay.io username: ${{ secrets.quay_username }} @@ -114,7 +114,7 @@ jobs: if: ${{ inputs.quay_image_name && inputs.push }} - name: Login to GitHub Container Registry - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 with: registry: ghcr.io username: ${{ secrets.ghcr_username }} @@ -122,7 +122,7 @@ jobs: if: ${{ inputs.ghcr_image_name && inputs.push }} - name: Login to dockerhub Container Registry - uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0 with: username: ${{ secrets.docker_username }} password: ${{ secrets.docker_password }} @@ -130,7 +130,7 @@ jobs: - name: Build and push container image id: image - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 #v5.1.0 + uses: docker/build-push-action@2cdde995de11925a030ce8070c3d77a52ffcf1c0 #v5.3.0 with: context: . platforms: ${{ inputs.platforms }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 53810510d3..d76724b1d7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -99,7 +99,7 @@ jobs: uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@0d103c3126aa41d772a8362f6aa67afac040f80c # v3.1.0 + uses: docker/setup-buildx-action@2b51285047da1547ffb1b2203d8be4c0af6b1f20 # v3.2.0 - name: Generate release artifacts run: | @@ -108,7 +108,7 @@ jobs: make manifests IMAGE_TAG=${{ github.ref_name }} - name: Draft release - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v0.1.15 with: tag_name: ${{ github.event.inputs.tag }} draft: true @@ -214,7 +214,7 @@ jobs: /tmp/sbom.tar.gz - name: Upload SBOM and signature assets - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # v0.1.15 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/analysis/controller.go b/analysis/controller.go index e8dcb8601a..1afa6ab556 100644 --- a/analysis/controller.go +++ b/analysis/controller.go @@ -7,6 +7,9 @@ import ( "github.com/argoproj/argo-rollouts/metric" jobProvider "github.com/argoproj/argo-rollouts/metricproviders/job" + "github.com/aws/smithy-go/ptr" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" unstructuredutil "github.com/argoproj/argo-rollouts/utils/unstructured" @@ -32,6 +35,10 @@ import ( timeutil "github.com/argoproj/argo-rollouts/utils/time" ) +var ( + analysisRunGVK = v1alpha1.SchemeGroupVersion.WithKind("AnalysisRun") +) + // Controller is the controller implementation for Analysis resources type Controller struct { // kubeclientset is a standard kubernetes clientset @@ -187,10 +194,24 @@ func (c *Controller) syncHandler(ctx context.Context, key string) error { return c.persistAnalysisRunStatus(run, newRun.Status) } -func (c *Controller) jobParentNamespace(obj any) string { +func (c *Controller) jobParentReference(obj any) (*v1.OwnerReference, string) { job, ok := obj.(*batchv1.Job) if !ok { - return "" + return nil, "" + } + // if it has owner reference, return it as is + ownerRef := v1.GetControllerOf(job) + // else if it's missing owner reference check if analysis run uid is set and + // if it is there use labels/annotations to create owner reference + if ownerRef == nil && job.Labels[jobProvider.AnalysisRunUIDLabelKey] != "" { + ownerRef = &v1.OwnerReference{ + APIVersion: analysisRunGVK.GroupVersion().String(), + Kind: analysisRunGVK.Kind, + Name: job.Annotations[jobProvider.AnalysisRunNameAnnotationKey], + UID: types.UID(job.Labels[jobProvider.AnalysisRunUIDLabelKey]), + BlockOwnerDeletion: ptr.Bool(true), + Controller: ptr.Bool(true), + } } ns := job.GetNamespace() if job.Annotations != nil { @@ -198,7 +219,7 @@ func (c *Controller) jobParentNamespace(obj any) string { ns = job.Annotations[jobProvider.AnalysisRunNamespaceAnnotationKey] } } - return ns + return ownerRef, ns } func (c *Controller) enqueueJobIfCompleted(obj any) { @@ -209,7 +230,7 @@ func (c *Controller) enqueueJobIfCompleted(obj any) { for _, condition := range job.Status.Conditions { switch condition.Type { case batchv1.JobFailed, batchv1.JobComplete: - controllerutil.EnqueueParentObject(job, register.AnalysisRunKind, c.enqueueAnalysis, c.jobParentNamespace) + controllerutil.EnqueueParentObject(job, register.AnalysisRunKind, c.enqueueAnalysis, c.jobParentReference) return } } diff --git a/cmd/rollouts-controller/main.go b/cmd/rollouts-controller/main.go index d198db3c31..4b058b09a7 100644 --- a/cmd/rollouts-controller/main.go +++ b/cmd/rollouts-controller/main.go @@ -142,7 +142,7 @@ func newCommand() *cobra.Command { instanceIDTweakListFunc := func(options *metav1.ListOptions) { options.LabelSelector = instanceIDSelector.String() } - jobKubeClient, err := metricproviders.GetAnalysisJobClientset(kubeClient) + jobKubeClient, _, err := metricproviders.GetAnalysisJobClientset(kubeClient) checkError(err) jobNs := metricproviders.GetAnalysisJobNamespace() if jobNs == "" { diff --git a/controller/controller.go b/controller/controller.go index 9aaf8ee09f..e9509cd78e 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -304,7 +304,7 @@ func NewManager( ingressWorkqueue := workqueue.NewNamedRateLimitingQueue(queue.DefaultArgoRolloutsRateLimiter(), "Ingresses") refResolver := rollout.NewInformerBasedWorkloadRefResolver(namespace, dynamicclientset, discoveryClient, argoprojclientset, rolloutsInformer.Informer()) - apiFactory := notificationapi.NewFactory(record.NewAPIFactorySettings(), defaults.Namespace(), notificationSecretInformerFactory.Core().V1().Secrets().Informer(), notificationConfigMapInformerFactory.Core().V1().ConfigMaps().Informer()) + apiFactory := notificationapi.NewFactory(record.NewAPIFactorySettings(analysisRunInformer), defaults.Namespace(), notificationSecretInformerFactory.Core().V1().Secrets().Informer(), notificationConfigMapInformerFactory.Core().V1().ConfigMaps().Informer()) recorder := record.NewEventRecorder(kubeclientset, metrics.MetricRolloutEventsTotal, metrics.MetricNotificationFailedTotal, metrics.MetricNotificationSuccessTotal, metrics.MetricNotificationSend, apiFactory) notificationsController := notificationcontroller.NewControllerWithNamespaceSupport(dynamicclientset.Resource(v1alpha1.RolloutGVR), rolloutsInformer.Informer(), apiFactory, notificationcontroller.WithToUnstructured(func(obj metav1.Object) (*unstructured.Unstructured, error) { diff --git a/controller/controller_test.go b/controller/controller_test.go index f724ef0712..0f94131c96 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -205,7 +205,7 @@ func (f *fixture) newManager(t *testing.T) *Manager { Recorder: record.NewFakeEventRecorder(), }) - apiFactory := notificationapi.NewFactory(record.NewAPIFactorySettings(), "default", k8sI.Core().V1().Secrets().Informer(), k8sI.Core().V1().ConfigMaps().Informer()) + apiFactory := notificationapi.NewFactory(record.NewAPIFactorySettings(i.Argoproj().V1alpha1().AnalysisRuns()), "default", k8sI.Core().V1().Secrets().Informer(), k8sI.Core().V1().ConfigMaps().Informer()) // rolloutsInformer := rolloutinformers.NewRolloutInformer(f.client, "", time.Minute, cache.Indexers{}) cm.notificationsController = notificationcontroller.NewController(dynamicClient.Resource(v1alpha1.RolloutGVR), i.Argoproj().V1alpha1().Rollouts().Informer(), apiFactory, notificationcontroller.WithToUnstructured(func(obj metav1.Object) (*unstructured.Unstructured, error) { diff --git a/docs/migrating.md b/docs/migrating.md index 3e1f85c461..524b67444d 100644 --- a/docs/migrating.md +++ b/docs/migrating.md @@ -55,10 +55,11 @@ Instead of removing Deployment you can scale it down to zero and reference it fr 1. Create a Rollout resource. 1. Reference an existing Deployment using `workloadRef` field. 1. In the `workloadRef` field set the `scaleDown` attribute, which specifies how the Deployment should be scaled down. There are three options available: -* "never": the Deployment is not scaled down -* "onsuccess": the Deployment is scaled down after the Rollout becomes healthy -* "progressively": as the Rollout is scaled up the Deployment is scaled down. -Alternatively, manually scale down an existing Deployment by changing replicas field of an existing Deployment to zero. + * `never`: the Deployment is not scaled down + * `onsuccess`: the Deployment is scaled down after the Rollout becomes healthy + * `progressively`: as the Rollout is scaled up the Deployment is scaled down. + + Alternatively, manually scale down an existing Deployment by changing replicas field of an existing Deployment to zero. 1. To perform an update, the change should be made to the Pod template field of the Deployment. Below is an example of a Rollout resource referencing a Deployment. diff --git a/go.mod b/go.mod index 2a2575d20f..b27a612a96 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,11 @@ require ( github.com/antonmedv/expr v1.15.5 github.com/argoproj/notifications-engine v0.4.1-0.20240219110818-7a069766e954 github.com/argoproj/pkg v0.13.6 - github.com/aws/aws-sdk-go-v2 v1.25.2 + github.com/aws/aws-sdk-go-v2 v1.25.3 github.com/aws/aws-sdk-go-v2/config v1.27.5 github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.1 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.1 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.3 + github.com/aws/smithy-go v1.20.1 github.com/blang/semver v3.5.1+incompatible github.com/bombsimon/logrusr/v4 v4.1.0 github.com/evanphx/json-patch/v5 v5.9.0 @@ -36,7 +37,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tj/assert v0.0.3 github.com/valyala/fasttemplate v1.2.2 - golang.org/x/oauth2 v0.17.0 + golang.org/x/oauth2 v0.18.0 google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 google.golang.org/grpc v1.62.1 google.golang.org/protobuf v1.32.0 @@ -79,8 +80,8 @@ require ( github.com/aws/aws-sdk-go v1.44.116 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.5 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.3 // indirect @@ -88,7 +89,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.2 // indirect - github.com/aws/smithy-go v1.20.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -173,11 +173,11 @@ require ( github.com/xlab/treeprint v1.1.0 // indirect go.opencensus.io v0.24.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect - golang.org/x/crypto v0.19.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/term v0.17.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.12.0 // indirect diff --git a/go.sum b/go.sum index 1ce6a216b4..94a8e016bc 100644 --- a/go.sum +++ b/go.sum @@ -92,24 +92,24 @@ github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2z github.com/aws/aws-sdk-go v1.44.39/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.116 h1:NpLIhcvLWXJZAEwvPj3TDHeqp7DleK6ZUVYyW01WNHY= github.com/aws/aws-sdk-go v1.44.116/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= -github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= +github.com/aws/aws-sdk-go-v2 v1.25.3 h1:xYiLpZTQs1mzvz5PaI6uR0Wh57ippuEthxS4iK5v0n0= +github.com/aws/aws-sdk-go-v2 v1.25.3/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I= github.com/aws/aws-sdk-go-v2/config v1.27.5 h1:brBPsyRFQn97M1ZhQ9tLXkO7Zytiar0NS06FGmEJBdg= github.com/aws/aws-sdk-go-v2/config v1.27.5/go.mod h1:I53uvsfddRRTG5YcC4n5Z3aOD1BU8hYCoIG7iEJG4wM= github.com/aws/aws-sdk-go-v2/credentials v1.17.5 h1:yn3zSvIKC2NZIs40cY3kckcy9Zma96PrRR07N54PCvY= github.com/aws/aws-sdk-go-v2/credentials v1.17.5/go.mod h1:8JcKPAGZVnDWuR5lusAwmrSDtZnDIAnpQWaDC9RFt2g= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3 h1:ifbIbHZyGl1alsAhPIYsHOg5MuApgqOvVeI8wIugXfs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.3/go.mod h1:oQZXg3c6SNeY6OZrDY+xHcF4VGIEoNotX2B4PrDeoJI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3 h1:Qvodo9gHG9F3E8SfYOspPeBt0bjSbsevK8WhRAUHcoY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.3/go.mod h1:vCKrdLXtybdf/uQd/YfVR2r5pcbNuEYKzMQpcxmeSJw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.1 h1:mQySuI87thHtcbZvEDjwUROGWikU6fqgpHklCBXpJU4= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.36.1/go.mod h1:Z1ThUUTuCO9PArtiQsTmBGBv+38NGj+795Zl0n1jgiM= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.1 h1:eDD7nyDlMxlkGhWu0n92LYkuSQIIEwN5CffwiMDohh0= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.1/go.mod h1:DGUI2cbxu24m0rNOm7DDmrCtTzR0U0FqE3XpBkR8+r8= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.3 h1:RdYkpKdapqc29UYKw7mGrDLpLRPJPERFO/ugLEMIhr8= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.3/go.mod h1:e0zaDIcMOQ48klOQQRw6xJJyi3F2zwmOUer8gHEFSbo= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.3 h1:x0N5ftQzgcfRpCpTiyZC40pvNUJYhzf4UgCsAyO6/P8= @@ -699,8 +699,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -784,8 +784,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -793,8 +793,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= -golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -876,8 +876,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -885,8 +885,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/metricproviders/job/job.go b/metricproviders/job/job.go index 8d76302149..5a85c5c71e 100644 --- a/metricproviders/job/job.go +++ b/metricproviders/job/job.go @@ -43,18 +43,20 @@ var ( ) type JobProvider struct { - kubeclientset kubernetes.Interface - jobLister batchlisters.JobLister - logCtx log.Entry - jobNamespace string + kubeclientset kubernetes.Interface + jobLister batchlisters.JobLister + logCtx log.Entry + jobNamespace string + customJobKubeconfig bool } -func NewJobProvider(logCtx log.Entry, kubeclientset kubernetes.Interface, jobLister batchlisters.JobLister, jobNS string) *JobProvider { +func NewJobProvider(logCtx log.Entry, kubeclientset kubernetes.Interface, jobLister batchlisters.JobLister, jobNS string, customJobKubeconfig bool) *JobProvider { return &JobProvider{ - kubeclientset: kubeclientset, - logCtx: logCtx, - jobLister: jobLister, - jobNamespace: jobNS, + kubeclientset: kubeclientset, + logCtx: logCtx, + jobLister: jobLister, + jobNamespace: jobNS, + customJobKubeconfig: customJobKubeconfig, } } @@ -85,7 +87,7 @@ func getJobIDSuffix(run *v1alpha1.AnalysisRun, metricName string) int { return int(res.Count + res.Error + 1) } -func newMetricJob(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, jobNS string) (*batchv1.Job, error) { +func newMetricJob(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, jobNS string, customJobKubeconfig bool) (*batchv1.Job, error) { ns := run.Namespace if jobNS != "" { ns = jobNS @@ -102,11 +104,17 @@ func newMetricJob(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric, jobNS strin jobAnnotations[AnalysisRunNameAnnotationKey] = run.Name jobAnnotations[AnalysisRunNamespaceAnnotationKey] = run.Namespace jobAnnotations[AnalysisRunMetricAnnotationKey] = metric.Name + + ownerRef := []metav1.OwnerReference{*metav1.NewControllerRef(run, analysisRunGVK)} + + if ns != run.Namespace || customJobKubeconfig { + ownerRef = nil + } job := batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: newJobName(run, metric), Namespace: ns, - OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(run, analysisRunGVK)}, + OwnerReferences: ownerRef, Annotations: jobAnnotations, Labels: jobLabels, }, @@ -122,7 +130,7 @@ func (p *JobProvider) Run(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric) v1a StartedAt: &now, Phase: v1alpha1.AnalysisPhaseRunning, } - job, err := newMetricJob(run, metric, p.jobNamespace) + job, err := newMetricJob(run, metric, p.jobNamespace, p.customJobKubeconfig) if err != nil { p.logCtx.Errorf("job initialization failed: %v", err) return metricutil.MarkMeasurementError(measurement, err) @@ -139,8 +147,17 @@ func (p *JobProvider) Run(run *v1alpha1.AnalysisRun, metric v1alpha1.Metric) v1a p.logCtx.Errorf("job create (verify) %s failed: %v", job.Name, createErr) return metricutil.MarkMeasurementError(measurement, createErr) } - controllerRef := metav1.GetControllerOf(existingJob) - if run.UID != controllerRef.UID { + ownerUID := "" + // if custom kubeconfig or different namespace is used owner ref is absent, + // use run uid label to get owner analysis run uid + if p.customJobKubeconfig || job.Namespace != run.Namespace { + ownerUID = job.Labels[AnalysisRunUIDLabelKey] + } else { + controllerRef := metav1.GetControllerOf(existingJob) + ownerUID = string(controllerRef.UID) + } + + if string(run.UID) != ownerUID { // NOTE: we don't bother to check for semantic equality. UID is good enough p.logCtx.Errorf("job create (uid check) %s failed: %v", job.Name, createErr) return metricutil.MarkMeasurementError(measurement, createErr) diff --git a/metricproviders/job/job_test.go b/metricproviders/job/job_test.go index c8ef2af3a0..80fdb28342 100644 --- a/metricproviders/job/job_test.go +++ b/metricproviders/job/job_test.go @@ -36,7 +36,7 @@ func newTestJobProvider(objects ...runtime.Object) *JobProvider { cancel() jobLister := k8sI.Batch().V1().Jobs().Lister() - return NewJobProvider(*logCtx, kubeclient, jobLister, "") + return NewJobProvider(*logCtx, kubeclient, jobLister, "", false) } func newRunWithJobMetric() *v1alpha1.AnalysisRun { @@ -193,7 +193,7 @@ func TestRunCreateCollision(t *testing.T) { p := newTestJobProvider() run := newRunWithJobMetric() - existingJob, err := newMetricJob(run, run.Spec.Metrics[0], p.jobNamespace) + existingJob, err := newMetricJob(run, run.Spec.Metrics[0], p.jobNamespace, p.customJobKubeconfig) assert.NoError(t, err) fakeClient := p.kubeclientset.(*k8sfake.Clientset) fakeClient.Tracker().Add(existingJob) diff --git a/metricproviders/metricproviders.go b/metricproviders/metricproviders.go index db135b82ca..916fc85aa2 100644 --- a/metricproviders/metricproviders.go +++ b/metricproviders/metricproviders.go @@ -52,12 +52,12 @@ func (f *ProviderFactory) NewProvider(logCtx log.Entry, metric v1alpha1.Metric) } return prometheus.NewPrometheusProvider(api, logCtx, metric) case job.ProviderType: - kubeClient, err := GetAnalysisJobClientset(f.KubeClient) + kubeClient, customKubeconfig, err := GetAnalysisJobClientset(f.KubeClient) if err != nil { return nil, err } - return job.NewJobProvider(logCtx, kubeClient, f.JobLister, GetAnalysisJobNamespace()), nil + return job.NewJobProvider(logCtx, kubeClient, f.JobLister, GetAnalysisJobNamespace(), customKubeconfig), nil case kayenta.ProviderType: c := kayenta.NewHttpClient() return kayenta.NewKayentaProvider(logCtx, c), nil @@ -154,7 +154,7 @@ func Type(metric v1alpha1.Metric) string { // if the AnalysisJobKubeconfigEnv is set to InclusterKubeconfig, it will return the incluster client // else if it's set to a kubeconfig file it will return the clientset corresponding to the kubeconfig file. // If empty it returns the provided defaultClientset -func GetAnalysisJobClientset(defaultClientset kubernetes.Interface) (kubernetes.Interface, error) { +func GetAnalysisJobClientset(defaultClientset kubernetes.Interface) (kubernetes.Interface, bool, error) { customJobKubeconfig := os.Getenv(AnalysisJobKubeconfigEnv) if customJobKubeconfig != "" { var ( @@ -167,11 +167,12 @@ func GetAnalysisJobClientset(defaultClientset kubernetes.Interface) (kubernetes. cfg, err = clientcmd.BuildConfigFromFlags("", customJobKubeconfig) } if err != nil { - return nil, err + return nil, true, err } - return kubernetes.NewForConfig(cfg) + clientSet, err := kubernetes.NewForConfig(cfg) + return clientSet, true, err } - return defaultClientset, nil + return defaultClientset, false, nil } func GetAnalysisJobNamespace() string { diff --git a/pkg/kubectl-argo-rollouts/cmd/cmd.go b/pkg/kubectl-argo-rollouts/cmd/cmd.go index 53adf27b5d..372f8c434d 100644 --- a/pkg/kubectl-argo-rollouts/cmd/cmd.go +++ b/pkg/kubectl-argo-rollouts/cmd/cmd.go @@ -56,6 +56,7 @@ func NewCmdArgoRollouts(o *options.ArgoRolloutsOptions) *cobra.Command { return o.UsageErr(c) }, } + o.AddKubectlFlags(cmd) cmd.AddCommand(create.NewCmdCreate(o)) cmd.AddCommand(get.NewCmdGet(o)) @@ -72,7 +73,7 @@ func NewCmdArgoRollouts(o *options.ArgoRolloutsOptions) *cobra.Command { cmd.AddCommand(undo.NewCmdUndo(o)) cmd.AddCommand(dashboard.NewCmdDashboard(o)) cmd.AddCommand(status.NewCmdStatus(o)) - cmd.AddCommand(notificationcmd.NewToolsCommand("notifications", "kubectl argo rollouts notifications", v1alpha1.RolloutGVR, record.NewAPIFactorySettings())) + cmd.AddCommand(notificationcmd.NewToolsCommand("notifications", "kubectl argo rollouts notifications", v1alpha1.RolloutGVR, record.NewAPIFactorySettings(nil))) cmd.AddCommand(completion.NewCmdCompletion(o)) return cmd diff --git a/rollout/analysis.go b/rollout/analysis.go index dec002697b..7bb0d47f1f 100644 --- a/rollout/analysis.go +++ b/rollout/analysis.go @@ -313,7 +313,7 @@ func (c *rolloutContext) reconcilePostPromotionAnalysisRun() (*v1alpha1.Analysis func (c *rolloutContext) reconcileBackgroundAnalysisRun() (*v1alpha1.AnalysisRun, error) { currentAr := c.currentArs.CanaryBackground - if c.rollout.Spec.Strategy.Canary.Analysis == nil { + if c.rollout.Spec.Strategy.Canary.Analysis == nil || len(c.rollout.Spec.Strategy.Canary.Analysis.Templates) == 0 { err := c.cancelAnalysisRuns([]*v1alpha1.AnalysisRun{currentAr}) return nil, err } diff --git a/rollout/analysis_test.go b/rollout/analysis_test.go index b46e553a10..b7995e564a 100644 --- a/rollout/analysis_test.go +++ b/rollout/analysis_test.go @@ -2733,4 +2733,47 @@ func concatMultipleSlices[T any](slices [][]T) []T { } return result + +} + +func TestCancelBackgroundAnalysisRunWhenRolloutAnalysisHasNoTemplate(t *testing.T) { + f := newFixture(t) + defer f.Close() + + at := analysisTemplate("bar") + steps := []v1alpha1.CanaryStep{ + {SetWeight: pointer.Int32Ptr(10)}, + } + + r1 := newCanaryRollout("foo", 1, nil, steps, pointer.Int32Ptr(1), intstr.FromInt(0), intstr.FromInt(1)) + rs1 := newReplicaSetWithStatus(r1, 1, 1) + rs1PodHash := rs1.Labels[v1alpha1.DefaultRolloutUniqueLabelKey] + r1 = updateCanaryRolloutStatus(r1, rs1PodHash, 1, 1, 1, false) + ar := analysisRun(at, v1alpha1.RolloutTypeStepLabel, r1) + r1.Status.Canary.CurrentBackgroundAnalysisRunStatus = &v1alpha1.RolloutAnalysisRunStatus{ + Name: ar.Name, + Status: v1alpha1.AnalysisPhaseRunning, + } + + r2 := bumpVersion(r1) + r2.Spec.Strategy.Canary.Analysis = &v1alpha1.RolloutAnalysisBackground{ + RolloutAnalysis: v1alpha1.RolloutAnalysis{}, // No templates provided. + } + rs2 := newReplicaSetWithStatus(r2, 0, 0) + + f.kubeobjects = append(f.kubeobjects, rs1, rs2) + f.replicaSetLister = append(f.replicaSetLister, rs1, rs2) + f.rolloutLister = append(f.rolloutLister, r2) + f.analysisTemplateLister = append(f.analysisTemplateLister, at) + f.analysisRunLister = append(f.analysisRunLister, ar) + f.objects = append(f.objects, r2, at, ar) + + _ = f.expectPatchAnalysisRunAction(ar) + patchIndex := f.expectPatchRolloutAction(r2) + _ = f.expectUpdateReplicaSetAction(rs1) + f.run(getKey(r2, t)) + + patch := f.getPatchedRollout(patchIndex) + + assert.Contains(t, patch, `"currentBackgroundAnalysisRunStatus":null`) } diff --git a/utils/annotations/annotations.go b/utils/annotations/annotations.go index 80cec5ce2a..121c6c0803 100644 --- a/utils/annotations/annotations.go +++ b/utils/annotations/annotations.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" "github.com/argoproj/argo-rollouts/utils/defaults" @@ -52,17 +53,30 @@ func GetWorkloadGenerationAnnotation(ro *v1alpha1.Rollout) (int32, bool) { } // GetRevisionAnnotation returns revision of rollout -func GetRevisionAnnotation(ro *v1alpha1.Rollout) (int32, bool) { - if ro == nil { +func GetRevisionAnnotation(anyObj metav1.Object) (int32, bool) { + + if anyObj == nil { + return 0, false + } + var obj interface{} + switch anyObj.(type) { + case *v1alpha1.Rollout: + obj = anyObj.(*v1alpha1.Rollout) + case *v1alpha1.AnalysisRun: + obj = anyObj.(*v1alpha1.AnalysisRun) + default: + log.Warnf("object not supported type: %T", anyObj) return 0, false } - annotationValue, ok := ro.Annotations[RevisionAnnotation] + + annotationValue, ok := obj.(metav1.Object).GetAnnotations()[RevisionAnnotation] if !ok { - return int32(0), false + return 0, false } + intValue, err := strconv.ParseInt(annotationValue, 10, 32) if err != nil { - log.Warnf("Cannot convert the value %q with annotation key %q for the replica set %q", annotationValue, RevisionAnnotation, ro.Name) + log.Warnf("Cannot convert the value %q with annotation key %q for the object %q", annotationValue, RevisionAnnotation, anyObj.GetName()) return int32(0), false } return int32(intValue), true diff --git a/utils/annotations/annotations_test.go b/utils/annotations/annotations_test.go index 3ad136fd7e..239ab83fd3 100644 --- a/utils/annotations/annotations_test.go +++ b/utils/annotations/annotations_test.go @@ -483,4 +483,16 @@ func TestGetRevisionAnnotation(t *testing.T) { }) assert.False(t, found) assert.Equal(t, int32(0), rev) + + revAR, found := GetRevisionAnnotation(&v1alpha1.AnalysisRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: metav1.NamespaceDefault, + Annotations: map[string]string{ + RevisionAnnotation: "1", + }, + }, + }) + assert.True(t, found) + assert.Equal(t, int32(1), revAR) } diff --git a/utils/controller/controller.go b/utils/controller/controller.go index 9c4b05cf26..2530e1d5fa 100644 --- a/utils/controller/controller.go +++ b/utils/controller/controller.go @@ -222,7 +222,7 @@ func EnqueueRateLimited(obj any, q workqueue.RateLimitingInterface) { // It then enqueues that ownerType resource to be processed. If the object does not // have an appropriate OwnerReference, it will simply be skipped. // This function assumes parent object is in the same namespace as the child -func EnqueueParentObject(obj any, ownerType string, enqueue func(obj any), parentNamespaceGetter ...func(any) string) { +func EnqueueParentObject(obj any, ownerType string, enqueue func(obj any), parentGetter ...func(any) (*metav1.OwnerReference, string)) { var object metav1.Object var ok bool if object, ok = obj.(metav1.Object); !ok { @@ -239,16 +239,25 @@ func EnqueueParentObject(obj any, ownerType string, enqueue func(obj any), paren log.Infof("Recovered deleted object '%s' from tombstone", object.GetName()) } - if ownerRef := metav1.GetControllerOf(object); ownerRef != nil { + var ( + ownerRef *metav1.OwnerReference + namespace string + ) + + if len(parentGetter) > 0 { + ownerRef, namespace = parentGetter[0](obj) + } else { + ownerRef = metav1.GetControllerOf(object) + } + + if ownerRef != nil { // If this object is not owned by the ownerType, we should not do anything more with it. if ownerRef.Kind != ownerType { return } - namespace := object.GetNamespace() - if len(parentNamespaceGetter) > 0 { - // If the parentNamespaceGetter is provided, use it to get the parent namespace - // only uses the first parentNamespaceGetter func - namespace = parentNamespaceGetter[0](obj) + // if namespace not set by parentGetter use object namespace + if namespace == "" { + namespace = object.GetNamespace() } parent := cache.ExplicitKey(namespace + "/" + ownerRef.Name) log.Infof("Enqueueing parent of %s/%s: %s %s", namespace, object.GetName(), ownerRef.Kind, parent) diff --git a/utils/record/record.go b/utils/record/record.go index 9e50f3ba65..801db089b6 100644 --- a/utils/record/record.go +++ b/utils/record/record.go @@ -7,11 +7,12 @@ import ( "encoding/json" "fmt" "regexp" + "sort" "strings" "time" + argoinformers "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/rollouts/v1alpha1" timeutil "github.com/argoproj/argo-rollouts/utils/time" - "github.com/argoproj/notifications-engine/pkg/api" "github.com/argoproj/notifications-engine/pkg/services" "github.com/argoproj/notifications-engine/pkg/subscriptions" @@ -20,6 +21,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" k8sinformers "k8s.io/client-go/informers" @@ -32,6 +34,7 @@ import ( "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" rolloutscheme "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/scheme" + "github.com/argoproj/argo-rollouts/utils/annotations" logutil "github.com/argoproj/argo-rollouts/utils/log" ) @@ -235,13 +238,83 @@ func (e *EventRecorderAdapter) K8sRecorder() record.EventRecorder { return e.Recorder } -func NewAPIFactorySettings() api.Settings { +func getAnalysisRunsFilterWithLabels(ro v1alpha1.Rollout, arInformer argoinformers.AnalysisRunInformer) (any, error) { + + set := labels.Set(map[string]string{ + v1alpha1.DefaultRolloutUniqueLabelKey: ro.Status.CurrentPodHash, + }) + + revision, _ := annotations.GetRevisionAnnotation(&ro) + ars, err := arInformer.Lister().AnalysisRuns(ro.Namespace).List(labels.SelectorFromSet(set)) + if err != nil { + return nil, fmt.Errorf("error getting analysisruns from informer for namespace: %s error: %w", ro.Namespace, err) + } + if len(ars) == 0 { + return nil, nil + } + + filteredArs := make([]*v1alpha1.AnalysisRun, 0, len(ars)) + for _, ar := range ars { + arRevision, _ := annotations.GetRevisionAnnotation(ar) + if arRevision == revision { + filteredArs = append(filteredArs, ar) + } + } + + sort.Slice(filteredArs, func(i, j int) bool { + ts1 := filteredArs[i].ObjectMeta.CreationTimestamp.Time + ts2 := filteredArs[j].ObjectMeta.CreationTimestamp.Time + return ts1.After(ts2) + }) + + var arsObj any + arBytes, err := json.Marshal(filteredArs) + + if err != nil { + return nil, fmt.Errorf("Failed to marshal analysisRuns for rollout revision: %s, err: %w", string(revision), err) + } + + err = json.Unmarshal(arBytes, &arsObj) + if err != nil { + return nil, fmt.Errorf("Failed to unmarshal analysisRuns for rollout revision: %s, err: %w", string(revision), err) + } + + return arsObj, nil +} + +func NewAPIFactorySettings(arInformer argoinformers.AnalysisRunInformer) api.Settings { return api.Settings{ SecretName: NotificationSecret, ConfigMapName: NotificationConfigMap, InitGetVars: func(cfg *api.Config, configMap *corev1.ConfigMap, secret *corev1.Secret) (api.GetVars, error) { return func(obj map[string]any, dest services.Destination) map[string]any { - return map[string]any{"rollout": obj, "time": timeExprs} + + var vars = map[string]any{"rollout": obj, "time": timeExprs} + + if arInformer == nil { + log.Infof("Notification is not set for analysisRun Informer: %s", dest) + return vars + } + + var ro v1alpha1.Rollout + err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj, &ro) + + if err != nil { + log.Errorf("unable to send notification: bad rollout object: %v", err) + return vars + } + + arsObj, err := getAnalysisRunsFilterWithLabels(ro, arInformer) + + if err != nil { + log.Errorf("Error calling getAnalysisRunsFilterWithLabels for namespace: %s", + ro.Namespace) + return vars + + } + + vars = map[string]any{"rollout": obj, "analysisRuns": arsObj, "time": timeExprs} + return vars }, nil }, } diff --git a/utils/record/record_test.go b/utils/record/record_test.go index 97f4452441..2cc7afd12c 100644 --- a/utils/record/record_test.go +++ b/utils/record/record_test.go @@ -2,6 +2,7 @@ package record import ( "bytes" + "encoding/json" "errors" "fmt" "strings" @@ -9,7 +10,11 @@ import ( "time" "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + argofake "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned/fake" + argoinformersfactory "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions" + argoinformers "github.com/argoproj/argo-rollouts/pkg/client/informers/externalversions/rollouts/v1alpha1" "github.com/argoproj/argo-rollouts/utils/defaults" + timeutil "github.com/argoproj/argo-rollouts/utils/time" "github.com/argoproj/notifications-engine/pkg/api" notificationapi "github.com/argoproj/notifications-engine/pkg/api" "github.com/argoproj/notifications-engine/pkg/mocks" @@ -21,13 +26,19 @@ import ( dto "github.com/prometheus/client_model/go" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" ) +var ( + noResyncPeriodFunc = func() time.Duration { return 0 } +) + func TestRecordLog(t *testing.T) { prevOutput := log.StandardLogger().Out defer func() { @@ -178,13 +189,18 @@ func TestSendNotificationsWhenConditionTime(t *testing.T) { k8sClient := fake.NewSimpleClientset() sharedInformers := informers.NewSharedInformerFactory(k8sClient, 0) + + f := argofake.NewSimpleClientset() + rolloutsI := argoinformersfactory.NewSharedInformerFactory(f, noResyncPeriodFunc()) + arInformer := rolloutsI.Argoproj().V1alpha1().AnalysisRuns() + cmInformer := sharedInformers.Core().V1().ConfigMaps().Informer() secretInformer := sharedInformers.Core().V1().Secrets().Informer() secretInformer.GetIndexer().Add(secret) cmInformer.GetIndexer().Add(cm) - apiFactory := notificationapi.NewFactory(NewAPIFactorySettings(), defaults.Namespace(), secretInformer, cmInformer) + apiFactory := notificationapi.NewFactory(NewAPIFactorySettings(arInformer), defaults.Namespace(), secretInformer, cmInformer) api, err := apiFactory.GetAPI() assert.NoError(t, err) @@ -224,8 +240,11 @@ func TestSendNotificationsWhenConditionTime(t *testing.T) { secretInformer.GetIndexer().Add(secret) cmInformer.GetIndexer().Add(cm) + f := argofake.NewSimpleClientset() + rolloutsI := argoinformersfactory.NewSharedInformerFactory(f, noResyncPeriodFunc()) + arInformer := rolloutsI.Argoproj().V1alpha1().AnalysisRuns() - apiFactory := notificationapi.NewFactory(NewAPIFactorySettings(), defaults.Namespace(), secretInformer, cmInformer) + apiFactory := notificationapi.NewFactory(NewAPIFactorySettings(arInformer), defaults.Namespace(), secretInformer, cmInformer) api, err := apiFactory.GetAPI() assert.NoError(t, err) @@ -422,17 +441,162 @@ func TestSendNotificationsNoTrigger(t *testing.T) { assert.Len(t, err, 1) } +func createAnalysisRunInformer(ars []*v1alpha1.AnalysisRun) argoinformers.AnalysisRunInformer { + f := argofake.NewSimpleClientset() + rolloutsI := argoinformersfactory.NewSharedInformerFactory(f, noResyncPeriodFunc()) + arInformer := rolloutsI.Argoproj().V1alpha1().AnalysisRuns() + for _, ar := range ars { + _ = arInformer.Informer().GetStore().Add(ar) + } + return arInformer +} + func TestNewAPIFactorySettings(t *testing.T) { - settings := NewAPIFactorySettings() - assert.Equal(t, NotificationConfigMap, settings.ConfigMapName) - assert.Equal(t, NotificationSecret, settings.SecretName) - getVars, err := settings.InitGetVars(nil, nil, nil) - assert.NoError(t, err) - rollout := map[string]any{"name": "hello"} - vars := getVars(rollout, services.Destination{}) + ars := []*v1alpha1.AnalysisRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "analysis-run-1", + CreationTimestamp: metav1.NewTime(timeutil.Now().Add(-1 * time.Hour)), + Namespace: "default", + Labels: map[string]string{"rollouts-pod-template-hash": "85659df978"}, + Annotations: map[string]string{"rollout.argoproj.io/revision": "1"}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "analysis-run-2", + CreationTimestamp: metav1.NewTime(timeutil.Now().Add(-2 * time.Hour)), + Namespace: "default", + Labels: map[string]string{"rollouts-pod-template-hash": "85659df978"}, + Annotations: map[string]string{"rollout.argoproj.io/revision": "1"}, + }, + }, + } + ro := v1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rollout", + Namespace: "default", + Annotations: map[string]string{"rollout.argoproj.io/revision": "1"}, + }, + Status: v1alpha1.RolloutStatus{ + CurrentPodHash: "85659df978", + }, + } + + type expectedFunc func(obj map[string]interface{}, ar any) map[string]interface{} + type arInformerFunc func([]*v1alpha1.AnalysisRun) argoinformers.AnalysisRunInformer + + testcase := []struct { + name string + arInformer arInformerFunc + rollout v1alpha1.Rollout + ars []*v1alpha1.AnalysisRun + expected expectedFunc + }{ + { + name: "Send notification with rollout and analysisRun", + arInformer: func(ars []*v1alpha1.AnalysisRun) argoinformers.AnalysisRunInformer { + return createAnalysisRunInformer(ars) + }, + rollout: ro, + ars: ars, + expected: func(obj map[string]interface{}, ar any) map[string]interface{} { + return map[string]interface{}{ + "rollout": obj, + "analysisRuns": ar, + "time": timeExprs, + } + }, + }, + { + name: "Send notification rollout when revision and label doesn't match", + arInformer: func(ars []*v1alpha1.AnalysisRun) argoinformers.AnalysisRunInformer { + return createAnalysisRunInformer(ars) + }, + rollout: ro, + ars: []*v1alpha1.AnalysisRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "analysis-run-3", + CreationTimestamp: metav1.NewTime(timeutil.Now().Add(-2 * time.Hour)), + Namespace: "default", + Labels: map[string]string{"rollouts-pod-template-hash": "1234"}, + Annotations: map[string]string{"rollout.argoproj.io/revision": "2"}, + }, + }, + }, + expected: func(obj map[string]interface{}, ar any) map[string]interface{} { + return map[string]interface{}{ + "rollout": obj, + "analysisRuns": nil, + "time": timeExprs, + } + }, + }, + { + name: "arInformer is nil", + arInformer: func(ars []*v1alpha1.AnalysisRun) argoinformers.AnalysisRunInformer { + return nil + }, + rollout: ro, + ars: nil, + expected: func(obj map[string]interface{}, ar any) map[string]interface{} { + return map[string]interface{}{ + "rollout": obj, + "time": timeExprs, + } + }, + }, + { + name: "analysisRuns nil for no matching namespace", + arInformer: func(ars []*v1alpha1.AnalysisRun) argoinformers.AnalysisRunInformer { + return createAnalysisRunInformer(ars) + }, + rollout: ro, + ars: []*v1alpha1.AnalysisRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "analysis-run-1", + CreationTimestamp: metav1.NewTime(timeutil.Now().Add(-2 * time.Hour)), + Namespace: "default-1", + Labels: map[string]string{"rollouts-pod-template-hash": "1234"}, + Annotations: map[string]string{"rollout.argoproj.io/revision": "2"}, + }, + }, + }, + expected: func(obj map[string]interface{}, ar any) map[string]interface{} { + return map[string]interface{}{ + "rollout": obj, + "analysisRuns": nil, + "time": timeExprs, + } + }, + }, + } - assert.Equal(t, map[string]any{"rollout": rollout, "time": timeExprs}, vars) + for _, test := range testcase { + t.Run(test.name, func(t *testing.T) { + + settings := NewAPIFactorySettings(test.arInformer(test.ars)) + getVars, err := settings.InitGetVars(nil, nil, nil) + require.NoError(t, err) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + obj, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(&test.rollout) + + arBytes, err := json.Marshal(test.ars) + var arsObj any + _ = json.Unmarshal(arBytes, &arsObj) + vars := getVars(obj, services.Destination{}) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + assert.Equal(t, test.expected(obj, arsObj), vars) + }) + } } func TestWorkloadRefObjectMap(t *testing.T) {