diff --git a/pkg/apis/v1alpha1/conditions.go b/pkg/apis/v1alpha1/conditions.go index cfdccf89b..1c7c14780 100644 --- a/pkg/apis/v1alpha1/conditions.go +++ b/pkg/apis/v1alpha1/conditions.go @@ -149,8 +149,9 @@ const ( // -- RUNNABLE ConditionTypes const ( - RunnableReady = "Ready" - RunTemplateReady = "RunTemplateReady" + RunnableReady = "Ready" + RunTemplateReady = "RunTemplateReady" + StampedObjectCondition = "StampedObjectCondition" ) // -- RUNNABLE ConditionType - RunTemplateReady ConditionReasons @@ -164,4 +165,6 @@ const ( FailedToListCreatedObjectsReason = "FailedToListCreatedObjects" UnknownErrorReason = "UnknownError" ClientBuilderErrorResourcesSubmittedReason = "ClientBuilderError" + SucceededStampedObjectConditionReason = "SucceededCondition" + UnknownStampedObjectConditionReason = "Unknown" ) diff --git a/pkg/conditions/runnable_conditions.go b/pkg/conditions/runnable_conditions.go index 21fa78ec0..7e9fd96e4 100644 --- a/pkg/conditions/runnable_conditions.go +++ b/pkg/conditions/runnable_conditions.go @@ -116,3 +116,21 @@ func ClientBuilderErrorCondition(err error) metav1.Condition { Message: err.Error(), } } + +// -- Runnable.Status.Conditions - StampedObjectCondition + +func StampedObjectConditionUnknown() metav1.Condition { + return metav1.Condition{ + Type: v1alpha1.StampedObjectCondition, + Status: metav1.ConditionUnknown, + Reason: v1alpha1.UnknownStampedObjectConditionReason, + } +} + +func StampedObjectConditionKnown(condition *metav1.Condition) metav1.Condition { + return metav1.Condition{ + Type: v1alpha1.StampedObjectCondition, + Status: condition.Status, + Reason: v1alpha1.SucceededStampedObjectConditionReason, + } +} diff --git a/pkg/controllers/runnable_reconciler.go b/pkg/controllers/runnable_reconciler.go index da8a78ff8..a4d2d5965 100644 --- a/pkg/controllers/runnable_reconciler.go +++ b/pkg/controllers/runnable_reconciler.go @@ -133,8 +133,15 @@ func (r *RunnableReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c r.conditionManager.AddPositive(conditions.RunTemplateReadyCondition()) } + var stampedObjectStatusPresent = false var trackingError error + if stampedObject != nil { + stampedCondition := utils.ExtractConditions(stampedObject).ConditionWithType("Succeeded") + if stampedCondition != nil { + r.conditionManager.AddPositive(conditions.StampedObjectConditionKnown(stampedCondition)) + stampedObjectStatusPresent = true + } trackingError = r.StampedTracker.Watch(log, stampedObject, &handler.EnqueueRequestForOwner{OwnerType: &v1alpha1.Runnable{}}) if trackingError != nil { log.Error(err, "failed to add informer for object", "object", stampedObject) @@ -143,6 +150,9 @@ func (r *RunnableReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c log.V(logger.DEBUG).Info("added informer for object", "object", stampedObject) } } + if !stampedObjectStatusPresent { + r.conditionManager.AddPositive(conditions.StampedObjectConditionUnknown()) + } return r.completeReconciliation(ctx, runnable, outputs, err) } diff --git a/pkg/realizer/healthcheck/healthcheck.go b/pkg/realizer/healthcheck/healthcheck.go index 5ba279738..c8ca4060f 100644 --- a/pkg/realizer/healthcheck/healthcheck.go +++ b/pkg/realizer/healthcheck/healthcheck.go @@ -15,7 +15,6 @@ package healthcheck import ( - "encoding/json" "fmt" corev1 "k8s.io/api/core/v1" @@ -26,6 +25,7 @@ import ( "github.com/vmware-tanzu/cartographer/pkg/conditions" "github.com/vmware-tanzu/cartographer/pkg/eval" "github.com/vmware-tanzu/cartographer/pkg/selector" + "github.com/vmware-tanzu/cartographer/pkg/utils" ) func IsClusterTemplate(reference *corev1.ObjectReference) bool { @@ -40,14 +40,14 @@ func IsClusterTemplate(reference *corev1.ObjectReference) bool { func OwnerHealthCondition(resourceStatuses []v1alpha1.ResourceStatus, previousConditions []metav1.Condition) metav1.Condition { var previousHealthCondition []metav1.Condition - condition := conditions.ConditionList(previousConditions).ConditionWithType(v1alpha1.ResourceHealthy) + condition := utils.ConditionList(previousConditions).ConditionWithType(v1alpha1.ResourceHealthy) if condition != nil { previousHealthCondition = append(previousHealthCondition, *condition) } healthyConditionManager := conditions.NewConditionManager(v1alpha1.ResourcesHealthy, previousHealthCondition) for _, resourceStatus := range resourceStatuses { - resourceHealthyCondition := conditions.ConditionList(resourceStatus.Conditions).ConditionWithType(v1alpha1.ResourceHealthy) + resourceHealthyCondition := utils.ConditionList(resourceStatus.Conditions).ConditionWithType(v1alpha1.ResourceHealthy) if resourceHealthyCondition != nil { healthyConditionManager.AddPositive(*resourceHealthyCondition) } @@ -90,7 +90,7 @@ func DetermineHealthCondition(rule *v1alpha1.HealthRule, realizedResource *v1alp } func singleConditionTypeCondition(singleConditionType string, stampedObject *unstructured.Unstructured) metav1.Condition { - singleCondition := extractConditions(stampedObject).ConditionWithType(singleConditionType) + singleCondition := utils.ExtractConditions(stampedObject).ConditionWithType(singleConditionType) if singleCondition != nil { if singleCondition.Status == metav1.ConditionFalse || singleCondition.Status == metav1.ConditionTrue { return conditions.SingleConditionMatchCondition(singleCondition.Status, singleConditionType, singleCondition.Message) @@ -113,22 +113,6 @@ func multiMatchCondition(multiMatchRule *v1alpha1.MultiMatchHealthRule, stampedO return conditions.MultiMatchNoMatchesCondition() } -func extractConditions(stampedObject *unstructured.Unstructured) conditions.ConditionList { - var conditionList conditions.ConditionList - maybeStatus := stampedObject.UnstructuredContent()["status"] - if unstructuredStatus, statusOk := maybeStatus.(map[string]interface{}); statusOk { - maybeConditions := unstructuredStatus["conditions"] - maybeConditionsJSON, err := json.Marshal(maybeConditions) - if err == nil { - err = json.Unmarshal(maybeConditionsJSON, &conditionList) - if err != nil { - return conditions.ConditionList{} - } - } - } - return conditionList -} - func messageForMatchingFieldRequirement(requirement v1alpha1.HealthMatchFieldSelectorRequirement, stampedObject *unstructured.Unstructured) string { evaluator := eval.EvaluatorBuilder() fieldValue, fieldErr := evaluator.EvaluateJsonPath(requirement.Key, stampedObject.UnstructuredContent()) @@ -145,7 +129,7 @@ func messageForMatchingFieldRequirement(requirement v1alpha1.HealthMatchFieldSel func anyUnhealthyMatchCondition(rule v1alpha1.HealthMatchRule, stampedObject *unstructured.Unstructured) *metav1.Condition { for _, conditionRule := range rule.MatchConditions { - singleCondition := extractConditions(stampedObject).ConditionWithType(conditionRule.Type) + singleCondition := utils.ExtractConditions(stampedObject).ConditionWithType(conditionRule.Type) if singleCondition != nil && singleCondition.Status == conditionRule.Status { condition := conditions.MultiMatchResourcesHealthyCondition(metav1.ConditionFalse, v1alpha1.MultiMatchConditionHealthyReason, @@ -169,7 +153,7 @@ func allHealthyMatchCondition(rule v1alpha1.HealthMatchRule, stampedObject *unst var firstReason string var message string for _, conditionRule := range rule.MatchConditions { - resourceCondition := extractConditions(stampedObject).ConditionWithType(conditionRule.Type) + resourceCondition := utils.ExtractConditions(stampedObject).ConditionWithType(conditionRule.Type) if resourceCondition == nil || resourceCondition.Status != conditionRule.Status { return nil } diff --git a/pkg/realizer/realizer.go b/pkg/realizer/realizer.go index 290af0090..0e4965bb6 100644 --- a/pkg/realizer/realizer.go +++ b/pkg/realizer/realizer.go @@ -26,11 +26,11 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/vmware-tanzu/cartographer/pkg/apis/v1alpha1" - "github.com/vmware-tanzu/cartographer/pkg/conditions" "github.com/vmware-tanzu/cartographer/pkg/logger" "github.com/vmware-tanzu/cartographer/pkg/realizer/healthcheck" "github.com/vmware-tanzu/cartographer/pkg/realizer/statuses" "github.com/vmware-tanzu/cartographer/pkg/templates" + "github.com/vmware-tanzu/cartographer/pkg/utils" ) func MakeSupplychainOwnerResources(supplyChain *v1alpha1.ClusterSupplyChain) []OwnerResource { @@ -125,7 +125,7 @@ func (r *realizer) Realize(ctx context.Context, resourceRealizer ResourceRealize var additionalConditions []metav1.Condition if (stampedObject == nil || template == nil) && previousResourceStatus != nil { realizedResource = &previousResourceStatus.RealizedResource - if previousResourceStatusHealthyCondition := conditions.ConditionList(previousResourceStatus.Conditions).ConditionWithType(v1alpha1.ResourceHealthy); previousResourceStatusHealthyCondition != nil { + if previousResourceStatusHealthyCondition := utils.ConditionList(previousResourceStatus.Conditions).ConditionWithType(v1alpha1.ResourceHealthy); previousResourceStatusHealthyCondition != nil { additionalConditions = []metav1.Condition{*previousResourceStatusHealthyCondition} } } else { diff --git a/pkg/realizer/runnable/realizer.go b/pkg/realizer/runnable/realizer.go index e68c6e1d0..5819af49d 100644 --- a/pkg/realizer/runnable/realizer.go +++ b/pkg/realizer/runnable/realizer.go @@ -99,8 +99,7 @@ func (r *runnableRealizer) Realize(ctx context.Context, runnable *v1alpha1.Runna } } - // FIXME: why are we taking a DeepCopy? - err = runnableRepo.EnsureImmutableObjectExistsOnCluster(ctx, stampedObject.DeepCopy(), map[string]string{"carto.run/runnable-name": runnable.Name}) + err = runnableRepo.EnsureImmutableObjectExistsOnCluster(ctx, stampedObject, map[string]string{"carto.run/runnable-name": runnable.Name}) if err != nil { log.Error(err, "failed to ensure object exists on cluster", "object", stampedObject) return nil, nil, errors.RunnableApplyStampedObjectError{ diff --git a/pkg/conditions/condition_utils.go b/pkg/utils/unstructured.go similarity index 54% rename from pkg/conditions/condition_utils.go rename to pkg/utils/unstructured.go index 82a22de5f..6fdb63903 100644 --- a/pkg/conditions/condition_utils.go +++ b/pkg/utils/unstructured.go @@ -12,9 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package conditions +package utils -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + "encoding/json" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) type ConditionList []metav1.Condition @@ -26,3 +31,19 @@ func (c ConditionList) ConditionWithType(conditionType string) *metav1.Condition } return nil } + +func ExtractConditions(stampedObject *unstructured.Unstructured) ConditionList { + var conditionList ConditionList + maybeStatus := stampedObject.UnstructuredContent()["status"] + if unstructuredStatus, statusOk := maybeStatus.(map[string]interface{}); statusOk { + maybeConditions := unstructuredStatus["conditions"] + maybeConditionsJSON, err := json.Marshal(maybeConditions) + if err == nil { + err = json.Unmarshal(maybeConditionsJSON, &conditionList) + if err != nil { + return ConditionList{} + } + } + } + return conditionList +} diff --git a/tests/integration/runnable/runnable_test.go b/tests/integration/runnable/runnable_test.go index 765bf9f56..79bb7dfee 100644 --- a/tests/integration/runnable/runnable_test.go +++ b/tests/integration/runnable/runnable_test.go @@ -17,22 +17,21 @@ package runnable_test import ( "context" "encoding/json" - "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + "github.com/vmware-tanzu/cartographer/pkg/apis/v1alpha1" + . "github.com/vmware-tanzu/cartographer/pkg/utils" + "github.com/vmware-tanzu/cartographer/tests/helpers" + "github.com/vmware-tanzu/cartographer/tests/resources" v1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apiserver/pkg/storage/names" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" - - "github.com/vmware-tanzu/cartographer/pkg/apis/v1alpha1" - . "github.com/vmware-tanzu/cartographer/pkg/utils" - "github.com/vmware-tanzu/cartographer/tests/helpers" - "github.com/vmware-tanzu/cartographer/tests/resources" ) var _ = Describe("Stamping a resource on Runnable Creation", func() { @@ -708,7 +707,7 @@ var _ = Describe("Stamping a resource on Runnable Creation", func() { }) }) - Describe("when a ClusterRunTemplate that produces a Resource leverages a Selected field", func() { + Describe("Latest stampedObject is the status", func() { BeforeEach(func() { runTemplateYaml := HereYamlF(` --- @@ -717,26 +716,18 @@ var _ = Describe("Stamping a resource on Runnable Creation", func() { metadata: name: my-run-template spec: + outputs: + test-status: status.conditions[?(@.type=="Succeeded")] template: - apiVersion: v1 - kind: ResourceQuota + apiVersion: test.run/v1alpha1 + kind: TestObj metadata: - generateName: my-stamped-resource- + generateName: test-crd- labels: - focus: something-useful + gen: "1" spec: - hard: - cpu: "1000" - memory: 200Gi - pods: "10" - scopeSelector: - matchExpressions: - - operator : In - scopeName: PriorityClass - values: [$(selected.spec.inputs.key)$] - `, - testNS, - ) + foo: $(runnable.spec.inputs.foo)$ + `) runTemplateDefinition = &unstructured.Unstructured{} err := yaml.Unmarshal([]byte(runTemplateYaml), runTemplateDefinition) @@ -745,11 +736,21 @@ var _ = Describe("Stamping a resource on Runnable Creation", func() { err = c.Create(ctx, runTemplateDefinition, &client.CreateOptions{}) Expect(err).NotTo(HaveOccurred()) }) - }) - Describe("Multiple objects created", func() { - BeforeEach(func() { - runnableYaml := HereYamlF(`--- + AfterEach(func() { + err := c.Delete(ctx, runTemplateDefinition) + Expect(err).NotTo(HaveOccurred()) + }) + + It("populates the runnable.Status.outputs properly", func() { + listOpts := []client.ListOption{ + client.InNamespace(testNS), + client.MatchingLabels(map[string]string{"carto.run/runnable-name": "my-runnable"}), + } + + var runnableObject = &v1alpha1.Runnable{} + By("creating the runnable", func() { + runnableYaml := HereYamlF(`--- apiVersion: carto.run/v1alpha1 kind: Runnable metadata: @@ -758,168 +759,284 @@ var _ = Describe("Stamping a resource on Runnable Creation", func() { labels: some-val: first spec: + retentionPolicy: {maxFailedRuns: 10, maxSuccessfulRuns: 10} serviceAccountName: %s + inputs: + foo: input-at-time-1 runTemplateRef: name: my-run-template namespace: %s kind: ClusterRunTemplate `, - testNS, serviceAccountName, testNS) - - runnableDefinition = &unstructured.Unstructured{} - err := yaml.Unmarshal([]byte(runnableYaml), runnableDefinition) - Expect(err).NotTo(HaveOccurred()) - - err = c.Create(ctx, runnableDefinition, &client.CreateOptions{}) - Expect(err).NotTo(HaveOccurred()) - - runTemplateYaml := HereYamlF(` - --- - apiVersion: carto.run/v1alpha1 - kind: ClusterRunTemplate - metadata: - name: my-run-template - spec: - outputs: - test-status: status.conditions[?(@.type=="Ready")] - template: - apiVersion: test.run/v1alpha1 - kind: TestObj - metadata: - generateName: test-crd- - labels: - gen: "1" - spec: - foo: "bar" - `) - - runTemplateDefinition = &unstructured.Unstructured{} - err = yaml.Unmarshal([]byte(runTemplateYaml), runTemplateDefinition) - Expect(err).NotTo(HaveOccurred()) - - err = c.Create(ctx, runTemplateDefinition, &client.CreateOptions{}) - Expect(err).NotTo(HaveOccurred()) - - opts := []client.ListOption{ - client.InNamespace(testNS), - client.MatchingLabels(map[string]string{"carto.run/runnable-name": "my-runnable"}), - } - - testsList := &resources.TestObjList{} - - Eventually(func() ([]resources.TestObj, error) { - err := c.List(ctx, testsList, opts...) - return testsList.Items, err - }).Should(HaveLen(1)) - - // This is in order to ensure gen 1 object and gen 2 object have different creationTimestamps - time.Sleep(time.Second) - - Expect(AlterFieldOfNestedStringMaps(runTemplateDefinition.Object, "spec.template.metadata.labels.gen", "2")).To(Succeed()) + testNS, serviceAccountName, testNS) - err = c.Update(ctx, runTemplateDefinition, &client.UpdateOptions{}) - Expect(err).NotTo(HaveOccurred()) + err := yaml.Unmarshal([]byte(runnableYaml), runnableObject) + Expect(err).NotTo(HaveOccurred()) - Eventually(func() ([]resources.TestObj, error) { - err := c.List(ctx, testsList, opts...) - return testsList.Items, err - }).Should(HaveLen(2)) + err = c.Create(ctx, runnableObject, &client.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + }) + By("showing that the Runnable status is unknown", func() { + Eventually(func() (v1alpha1.RunnableStatus, error) { + runnable := &v1alpha1.Runnable{} + err := c.Get(ctx, client.ObjectKey{Namespace: testNS, Name: "my-runnable"}, runnable) + return runnable.Status, err + }).Should( + MatchFields(IgnoreExtras, + Fields{ + "Conditions": ContainElements( + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("Ready"), + "Status": Equal(metav1.ConditionUnknown), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("StampedObjectCondition"), + "Status": Equal(metav1.ConditionUnknown), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("RunTemplateReady"), + "Status": Equal(metav1.ConditionTrue), + }, + ), + ), + }, + ), + ) + }) - // This is in order to ensure gen 2 object and gen 3 object have different creationTimestamps - // Gen 3 object is needed to demonstrate behaviour when the most recently submitted is not successful - time.Sleep(time.Second) + var firstStampedObject resources.TestObj + var secondStampedObject resources.TestObj - Expect(AlterFieldOfNestedStringMaps(runTemplateDefinition.Object, "spec.template.metadata.labels.gen", "3")).To(Succeed()) + By("seeing one stamped object", func() { + testsList := &resources.TestObjList{} - err = c.Update(ctx, runTemplateDefinition, &client.UpdateOptions{}) - Expect(err).NotTo(HaveOccurred()) + Eventually(func() ([]resources.TestObj, error) { + err := c.List(ctx, testsList, listOpts...) + return testsList.Items, err + }).Should(HaveLen(1)) - Eventually(func() ([]resources.TestObj, error) { - err := c.List(ctx, testsList, opts...) - return testsList.Items, err - }).Should(HaveLen(3)) - }) + firstStampedObject = testsList.Items[0] + }) + By("changing the first stamped object's status to false", func() { + firstStampedObject.Status = resources.TestStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + { + Type: "Succeeded", + Status: "False", + ObservedGeneration: 1, + LastTransitionTime: metav1.Now(), + Reason: "FirstStampFailed", + Message: "not a happy first stamped object", + }, + }, + } + + Eventually(func() error { + return c.Status().Update(ctx, &firstStampedObject) + }).ShouldNot(HaveOccurred()) + }) + By("seeing that the runnable status is false", func() { + Eventually(func() ([]metav1.Condition, error) { + runnable := &v1alpha1.Runnable{} + err := c.Get(ctx, client.ObjectKey{Namespace: testNS, Name: "my-runnable"}, runnable) + return runnable.Status.Conditions, err + }).Should( + ContainElements( + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("Ready"), + "Status": Equal(metav1.ConditionFalse), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("StampedObjectCondition"), + "Status": Equal(metav1.ConditionFalse), + "Reason": Equal("SucceededCondition"), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("RunTemplateReady"), + "Status": Equal(metav1.ConditionTrue), + }, + ), + ), + ) + }) - AfterEach(func() { - err := c.Delete(ctx, runnableDefinition) - Expect(err).NotTo(HaveOccurred()) + By("changing the input for the Runnable", func() { + Eventually(func() error { + err := c.Get(ctx, client.ObjectKey{ + Namespace: runnableObject.GetNamespace(), + Name: runnableObject.GetName(), + }, runnableObject) - err = c.Delete(ctx, runTemplateDefinition) - Expect(err).NotTo(HaveOccurred()) - }) - - It("populates the runnable.Status.outputs properly", func() { - By("updating runnable status based on the most recently submitted and successful object") - opts := []client.ListOption{ - client.InNamespace(testNS), - client.MatchingLabels(map[string]string{"gen": "2"}), - } + if err != nil { + return err + } - testsList := &resources.TestObjList{} + runnableObject.Spec.Inputs["foo"] = apiextensionsv1.JSON{ + Raw: []byte(`"input-at-time-2"`), + } - Eventually(func() ([]resources.TestObj, error) { - err := c.List(ctx, testsList, opts...) - return testsList.Items, err - }).Should(HaveLen(1)) + return c.Update(ctx, runnableObject, &client.UpdateOptions{}) + }).ShouldNot(HaveOccurred()) + }) + By("seeing that there is a new stampedObject", func() { + testsList := &resources.TestObjList{} + Eventually(func() ([]resources.TestObj, error) { + err := c.List(ctx, testsList, listOpts...) + return testsList.Items, err + }).Should(HaveLen(2)) + + for _, so := range testsList.Items { + if so.Spec.Foo == "input-at-time-2" { + secondStampedObject = so + continue + } + } + }) + By("seeing that the runnable status is unknown", func() { + Eventually(func() ([]metav1.Condition, error) { + runnable := &v1alpha1.Runnable{} + err := c.Get(ctx, client.ObjectKey{Namespace: testNS, Name: "my-runnable"}, runnable) + return runnable.Status.Conditions, err + }).Should( + ContainElements( + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("Ready"), + "Status": Equal(metav1.ConditionUnknown), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("StampedObjectCondition"), + "Status": Equal(metav1.ConditionUnknown), + "Reason": Equal("Unknown"), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("RunTemplateReady"), + "Status": Equal(metav1.ConditionTrue), + }, + ), + ), + ) + }) + By("changing the second stampedObject's status to True", func() { + secondStampedObject.Status = resources.TestStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + { + Type: "Succeeded", + Status: "True", + ObservedGeneration: 1, + LastTransitionTime: metav1.Now(), + Reason: "SecondStampWorked", + Message: "happy second stamped object", + }, + }, + } + + Eventually(func() error { + return c.Status().Update(ctx, &secondStampedObject) + }).ShouldNot(HaveOccurred()) + }) + By("seeing that the runnable status is true", func() { + Eventually(func() ([]metav1.Condition, error) { + runnable := &v1alpha1.Runnable{} + err := c.Get(ctx, client.ObjectKey{Namespace: testNS, Name: "my-runnable"}, runnable) + return runnable.Status.Conditions, err + }).Should( + ContainElements( + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("Ready"), + "Status": Equal(metav1.ConditionTrue), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("StampedObjectCondition"), + "Status": Equal(metav1.ConditionTrue), + "Reason": Equal("SucceededCondition"), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("RunTemplateReady"), + "Status": Equal(metav1.ConditionTrue), + }, + ), + ), + ) - testToUpdate := &testsList.Items[0] - testToUpdate.Status.Conditions = []metav1.Condition{ - { - Type: "Ready", - Status: "True", - Reason: "LifeIsGood", - LastTransitionTime: metav1.Now(), - Message: "this is generation 2", - }, - { - Type: "Succeeded", - Status: "True", - Reason: "Success", - LastTransitionTime: metav1.Now(), - }, - } - err := c.Status().Update(ctx, testToUpdate) - Expect(err).NotTo(HaveOccurred()) + }) - Eventually(getRunnableTestStatus).Should(MatchFields(IgnoreExtras, Fields{ - "Message": Equal("this is generation 2"), - })) + By("changing the input for the Runnable", func() { + Eventually(func() error { + err := c.Get(ctx, client.ObjectKey{ + Namespace: runnableObject.GetNamespace(), + Name: runnableObject.GetName(), + }, runnableObject) - By("not updating runnable status based on the less recently submitted and successful objects") - opts = []client.ListOption{ - client.InNamespace(testNS), - client.MatchingLabels(map[string]string{"gen": "1"}), - } + if err != nil { + return err + } - Eventually(func() ([]resources.TestObj, error) { - err := c.List(ctx, testsList, opts...) - return testsList.Items, err - }).Should(HaveLen(1)) + runnableObject.Spec.Inputs["foo"] = apiextensionsv1.JSON{ + Raw: []byte(`"input-at-time-3"`), + } - testToUpdate = &testsList.Items[0] - testToUpdate.Status.Conditions = []metav1.Condition{ - { - Type: "Ready", - Status: "True", - Reason: "LifeIsGood", - LastTransitionTime: metav1.Now(), - Message: "but this is earlier generation 1", - }, - { - Type: "Succeeded", - Status: "True", - Reason: "Success", - LastTransitionTime: metav1.Now(), - }, - } - err = c.Status().Update(ctx, testToUpdate) - Expect(err).NotTo(HaveOccurred()) + return c.Update(ctx, runnableObject, &client.UpdateOptions{}) + }).ShouldNot(HaveOccurred()) - Consistently(getRunnableTestStatus, "1s").Should(MatchFields(IgnoreExtras, Fields{ - "Message": And( - Equal("this is generation 2"), - Not(Equal("but this is earlier generation 1"))), - })) + }) + By("seeing that there is a new stampedObject", func() { + testsList := &resources.TestObjList{} + Eventually(func() ([]resources.TestObj, error) { + err := c.List(ctx, testsList, listOpts...) + return testsList.Items, err + }).Should(HaveLen(3)) + }) + By("seeing that the runnable status is unknown", func() { + Eventually(func() ([]metav1.Condition, error) { + runnable := &v1alpha1.Runnable{} + err := c.Get(ctx, client.ObjectKey{Namespace: testNS, Name: "my-runnable"}, runnable) + return runnable.Status.Conditions, err + }).Should( + ContainElements( + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("Ready"), + "Status": Equal(metav1.ConditionUnknown), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("StampedObjectCondition"), + "Status": Equal(metav1.ConditionUnknown), + "Reason": Equal("Unknown"), + }, + ), + MatchFields(IgnoreExtras, + Fields{ + "Type": Equal("RunTemplateReady"), + "Status": Equal(metav1.ConditionTrue), + }, + ), + ), + ) + }) }) }) })