package kubernetes import ( "testing" apiv1beta1 "github.com/docker/compose-on-kubernetes/api/compose/v1beta1" composelabels "github.com/docker/compose-on-kubernetes/api/labels" "gotest.tools/v3/assert" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/watch" k8stesting "k8s.io/client-go/testing" ) var podsResource = apiv1.SchemeGroupVersion.WithResource("pods") var podKind = apiv1.SchemeGroupVersion.WithKind("Pod") var stacksResource = apiv1beta1.SchemeGroupVersion.WithResource("stacks") var stackKind = apiv1beta1.SchemeGroupVersion.WithKind("Stack") type testPodAndStackRepository struct { fake *k8stesting.Fake } func (r *testPodAndStackRepository) stackListWatchForNamespace(ns string) *testStackListWatch { return &testStackListWatch{fake: r.fake, ns: ns} } func (r *testPodAndStackRepository) podListWatchForNamespace(ns string) *testPodListWatch { return &testPodListWatch{fake: r.fake, ns: ns} } func newTestPodAndStackRepository(initialPods []apiv1.Pod, initialStacks []apiv1beta1.Stack, podWatchHandler, stackWatchHandler k8stesting.WatchReactionFunc) *testPodAndStackRepository { var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) apiv1.AddToScheme(scheme) apiv1beta1.AddToScheme(scheme) o := k8stesting.NewObjectTracker(scheme, codecs.UniversalDecoder()) for _, obj := range initialPods { obj := obj if err := o.Add(&obj); err != nil { panic(err) } } for _, obj := range initialStacks { obj := obj if err := o.Add(&obj); err != nil { panic(err) } } fakePtr := &k8stesting.Fake{} fakePtr.AddReactor("*", "*", k8stesting.ObjectReaction(o)) if podWatchHandler != nil { fakePtr.AddWatchReactor(podsResource.Resource, podWatchHandler) } if stackWatchHandler != nil { fakePtr.AddWatchReactor(stacksResource.Resource, stackWatchHandler) } fakePtr.AddWatchReactor("*", k8stesting.DefaultWatchReactor(watch.NewFake(), nil)) return &testPodAndStackRepository{fake: fakePtr} } type testStackListWatch struct { fake *k8stesting.Fake ns string } func (s *testStackListWatch) List(opts metav1.ListOptions) (*apiv1beta1.StackList, error) { obj, err := s.fake.Invokes(k8stesting.NewListAction(stacksResource, stackKind, s.ns, opts), &apiv1beta1.StackList{}) if obj == nil { return nil, err } label, _, _ := k8stesting.ExtractFromListOptions(opts) if label == nil { label = labels.Everything() } list := &apiv1beta1.StackList{} for _, item := range obj.(*apiv1beta1.StackList).Items { if label.Matches(labels.Set(item.Labels)) { list.Items = append(list.Items, item) } } return list, err } func (s *testStackListWatch) Watch(opts metav1.ListOptions) (watch.Interface, error) { return s.fake.InvokesWatch(k8stesting.NewWatchAction(stacksResource, s.ns, opts)) } type testPodListWatch struct { fake *k8stesting.Fake ns string } func (p *testPodListWatch) List(opts metav1.ListOptions) (*apiv1.PodList, error) { obj, err := p.fake.Invokes(k8stesting.NewListAction(podsResource, podKind, p.ns, opts), &apiv1.PodList{}) if obj == nil { return nil, err } label, _, _ := k8stesting.ExtractFromListOptions(opts) if label == nil { label = labels.Everything() } list := &apiv1.PodList{} for _, item := range obj.(*apiv1.PodList).Items { if label.Matches(labels.Set(item.Labels)) { list.Items = append(list.Items, item) } } return list, err } func (p *testPodListWatch) Watch(opts metav1.ListOptions) (watch.Interface, error) { return p.fake.InvokesWatch(k8stesting.NewWatchAction(podsResource, p.ns, opts)) } func TestDeployWatchOk(t *testing.T) { stack := apiv1beta1.Stack{ ObjectMeta: metav1.ObjectMeta{Name: "test-stack", Namespace: "test-ns"}, } serviceNames := []string{"svc1", "svc2"} testRepo := newTestPodAndStackRepository(nil, []apiv1beta1.Stack{stack}, func(action k8stesting.Action) (handled bool, ret watch.Interface, err error) { res := watch.NewFake() go func() { pod1 := &apiv1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test1", Namespace: "test-ns", Labels: composelabels.ForService("test-stack", "svc1"), }, Status: apiv1.PodStatus{ Phase: apiv1.PodRunning, Conditions: []apiv1.PodCondition{ { Type: apiv1.PodReady, Status: apiv1.ConditionTrue, }, }, }, } pod2 := &apiv1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test2", Namespace: "test-ns", Labels: composelabels.ForService("test-stack", "svc2"), }, Status: apiv1.PodStatus{ Phase: apiv1.PodRunning, Conditions: []apiv1.PodCondition{ { Type: apiv1.PodReady, Status: apiv1.ConditionTrue, }, }, }, } res.Add(pod1) res.Add(pod2) }() return true, res, nil }, nil) testee := &deployWatcher{ stacks: testRepo.stackListWatchForNamespace("test-ns"), pods: testRepo.podListWatchForNamespace("test-ns"), } statusUpdates := make(chan serviceStatus) go func() { for range statusUpdates { } }() defer close(statusUpdates) err := testee.Watch(stack.Name, serviceNames, statusUpdates) assert.NilError(t, err) } func TestDeployReconcileFailure(t *testing.T) { stack := apiv1beta1.Stack{ ObjectMeta: metav1.ObjectMeta{Name: "test-stack", Namespace: "test-ns"}, } serviceNames := []string{"svc1", "svc2"} testRepo := newTestPodAndStackRepository(nil, []apiv1beta1.Stack{stack}, nil, func(action k8stesting.Action) (handled bool, ret watch.Interface, err error) { res := watch.NewFake() go func() { sfailed := stack sfailed.Status = apiv1beta1.StackStatus{ Phase: apiv1beta1.StackFailure, Message: "test error", } res.Modify(&sfailed) }() return true, res, nil }) testee := &deployWatcher{ stacks: testRepo.stackListWatchForNamespace("test-ns"), pods: testRepo.podListWatchForNamespace("test-ns"), } statusUpdates := make(chan serviceStatus) go func() { for range statusUpdates { } }() defer close(statusUpdates) err := testee.Watch(stack.Name, serviceNames, statusUpdates) assert.ErrorContains(t, err, "Failure: test error") }