package kubernetes import ( "testing" "time" appsv1 "k8s.io/api/apps/v1" 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" ) // conditionTime is the cluster's own stamp for "this rollout completed", // pinned so assertions can compare nanoseconds exactly. var conditionTime = metav1.NewTime(time.Unix(1700000000, 0)) func testDeployment(overrides func(*appsv1.Deployment)) *appsv1.Deployment { created := metav1.NewTime(time.Now().Add(-2 * time.Hour)) deployReplicas := int32(3) deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Namespace: "shop", Name: "checkout", Generation: 3, CreationTimestamp: created, Annotations: map[string]string{ "deployment.kubernetes.io/revision": "7", }, }, Spec: appsv1.DeploymentSpec{Replicas: &deployReplicas}, Status: appsv1.DeploymentStatus{ Replicas: 3, UpdatedReplicas: 3, AvailableReplicas: 3, ReadyReplicas: 3, Conditions: []appsv1.DeploymentCondition{ { Type: appsv1.DeploymentProgressing, Status: corev1.ConditionTrue, Reason: "NewReplicaSetAvailable", Message: "ReplicaSet \"checkout-abc\" has successfully progressed.", LastUpdateTime: conditionTime, LastTransitionTime: conditionTime, }, { Type: appsv1.DeploymentAvailable, Status: corev1.ConditionTrue, LastUpdateTime: conditionTime, LastTransitionTime: conditionTime, }, }, }, } if overrides != nil { overrides(deployment) } return deployment } func observeDeployment(w *Watcher, deployment *appsv1.Deployment) { w.observe(deployment) } func TestObserveCompletedRolloutKeepsTimesAcrossResync(t *testing.T) { w := &Watcher{rollouts: make(map[string]Rollout), revisions: make(map[string]int64)} observeDeployment(w, testDeployment(nil)) first, ok := w.rollouts["shop/checkout/7"] if !ok { t.Fatal("rollout not recorded") } if first.Phase != "completed" { t.Fatalf("phase = %q, want completed", first.Phase) } if first.CompletedAt == 0 { t.Fatal("completed rollout missing CompletedAt") } // The Progressing condition's update time is the cluster's completion stamp, // not "now". if first.CompletedAt != conditionTime.UnixNano() { t.Fatalf("CompletedAt = %d, want the condition time %d", first.CompletedAt, conditionTime.UnixNano()) } // A resync ~30s later re-delivers the same deployment. A quiet rollout must // keep its times instead of looking perpetually fresh. time.Sleep(2 * time.Millisecond) observeDeployment(w, testDeployment(nil)) second := w.rollouts["shop/checkout/7"] if second.LastSeen != first.LastSeen { t.Fatalf("LastSeen advanced from %d to %d on an unchanged resync", first.LastSeen, second.LastSeen) } if second.CompletedAt != first.CompletedAt { t.Fatalf("CompletedAt re-bumped from %d to %d on an unchanged resync", first.CompletedAt, second.CompletedAt) } // Any observable change does advance LastSeen. changed := testDeployment(func(d *appsv1.Deployment) { d.Status.ReadyReplicas = 2 }) observeDeployment(w, changed) third := w.rollouts["shop/checkout/7"] if third.LastSeen == first.LastSeen { t.Fatal("LastSeen did not advance after an observable change") } } func TestObserveStartedAtPrefersReplicaSetCreation(t *testing.T) { w := &Watcher{rollouts: make(map[string]Rollout), revisions: make(map[string]int64)} rolloutStart := time.Now().Add(-48 * time.Hour) w.observeReplicaSet(&appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Namespace: "shop", Name: "checkout-abc", CreationTimestamp: metav1.NewTime(rolloutStart), OwnerReferences: []metav1.OwnerReference{{Kind: "Deployment", Name: "checkout"}}, Annotations: map[string]string{"deployment.kubernetes.io/revision": "7"}, }, }) observeDeployment(w, testDeployment(nil)) rollout := w.rollouts["shop/checkout/7"] if rollout.StartedAt != rolloutStart.UnixNano() { t.Fatalf("StartedAt = %d, want ReplicaSet creation %d", rollout.StartedAt, rolloutStart.UnixNano()) } } func TestObserveFirstSeenBeforeReplicaSetInformerRecovers(t *testing.T) { w := &Watcher{rollouts: make(map[string]Rollout), revisions: make(map[string]int64)} // Deployment events can arrive before the ReplicaSet cache has synced; the // recorded start must be corrected once the ReplicaSet timestamp appears. observeDeployment(w, testDeployment(nil)) stale := w.rollouts["shop/checkout/7"] rolloutStart := time.Now().Add(-24 * time.Hour) w.observeReplicaSet(&appsv1.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ Namespace: "shop", Name: "checkout-abc", CreationTimestamp: metav1.NewTime(rolloutStart), OwnerReferences: []metav1.OwnerReference{{Kind: "Deployment", Name: "checkout"}}, Annotations: map[string]string{"deployment.kubernetes.io/revision": "7"}, }, }) observeDeployment(w, testDeployment(nil)) rollout := w.rollouts["shop/checkout/7"] if rollout.StartedAt != rolloutStart.UnixNano() { t.Fatalf("StartedAt = %d, want corrected ReplicaSet creation %d", rollout.StartedAt, rolloutStart.UnixNano()) } if stale.StartedAt == 0 { t.Fatal("first observation should still record a fallback start") } } func TestListSortsNewestRolloutFirst(t *testing.T) { w := &Watcher{rollouts: make(map[string]Rollout), revisions: make(map[string]int64)} old := testDeployment(nil) old.Annotations["deployment.kubernetes.io/revision"] = "6" old.CreationTimestamp = metav1.NewTime(time.Now().Add(-72 * time.Hour)) recent := testDeployment(nil) observeDeployment(w, old) observeDeployment(w, recent) list := w.List() if len(list) != 2 { t.Fatalf("len(List) = %d, want 2", len(list)) } if list[0].Revision != "7" || list[1].Revision != "6" { t.Fatalf("List order = [%s, %s], want newest rollout first", list[0].Revision, list[1].Revision) } } // Deletions and tombstones must not panic the handlers. func TestObserveIgnoresNonDeploymentObjects(t *testing.T) { w := &Watcher{rollouts: make(map[string]Rollout), revisions: make(map[string]int64)} w.observe(&unstructured.Unstructured{}) w.observeReplicaSet(runtime.Object(&unstructured.Unstructured{})) if len(w.List()) != 0 { t.Fatal("no rollouts should be recorded") } }