github.com/docker/compose-on-kubernetes@v0.5.0/internal/controller/stackreconciler_test.go (about)

     1  package controller
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"sync"
     9  	"testing"
    10  
    11  	"github.com/docker/compose-on-kubernetes/api/compose/latest"
    12  	"github.com/docker/compose-on-kubernetes/internal/stackresources"
    13  	"github.com/docker/compose-on-kubernetes/internal/stackresources/diff"
    14  	. "github.com/docker/compose-on-kubernetes/internal/test/builders"
    15  	"github.com/stretchr/testify/assert"
    16  	coretypes "k8s.io/api/core/v1"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  )
    19  
    20  type testChildrenStore struct {
    21  	initialStackState *stackresources.StackState
    22  }
    23  
    24  func (s *testChildrenStore) getCurrentStackState(_ string) (*stackresources.StackState, error) {
    25  	return s.initialStackState, nil
    26  }
    27  
    28  func newTestChildrenStore(objects ...interface{}) (*testChildrenStore, error) {
    29  	state, err := stackresources.NewStackState(objects...)
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  	return &testChildrenStore{state}, err
    34  }
    35  
    36  type testStackStore struct {
    37  	originalStack *latest.Stack
    38  }
    39  
    40  func (s *testStackStore) get(_ string) (*latest.Stack, error) {
    41  	return s.originalStack, nil
    42  }
    43  
    44  func newTestStackStore(originalStack *latest.Stack) *testStackStore {
    45  	return &testStackStore{originalStack: originalStack}
    46  }
    47  
    48  type testResourceUpdaterProvider struct {
    49  	diffs                chan<- *diff.StackStateDiff
    50  	statuses             chan<- *latest.Stack
    51  	deleteChildResources chan<- struct{}
    52  }
    53  
    54  func (p *testResourceUpdaterProvider) getUpdater(stack *latest.Stack, _ bool) (resourceUpdater, error) {
    55  	return &testResourceUpdater{
    56  		diffs:                p.diffs,
    57  		statuses:             p.statuses,
    58  		stack:                stack,
    59  		deleteChildResources: p.deleteChildResources,
    60  	}, nil
    61  }
    62  
    63  func newTestResourceUpdaterProvider(diffs chan<- *diff.StackStateDiff,
    64  	statuses chan<- *latest.Stack,
    65  	deleteChildResources chan<- struct{}) *testResourceUpdaterProvider {
    66  	return &testResourceUpdaterProvider{diffs: diffs, statuses: statuses, deleteChildResources: deleteChildResources}
    67  }
    68  
    69  type testResourceUpdater struct {
    70  	diffs                chan<- *diff.StackStateDiff
    71  	statuses             chan<- *latest.Stack
    72  	deleteChildResources chan<- struct{}
    73  	stack                *latest.Stack
    74  }
    75  
    76  func (u *testResourceUpdater) applyStackDiff(diff *diff.StackStateDiff) error {
    77  	u.diffs <- diff
    78  	return nil
    79  }
    80  func (u *testResourceUpdater) updateStackStatus(status latest.StackStatus) (*latest.Stack, error) {
    81  	if u.stack.Status != nil && *u.stack.Status == status {
    82  		return u.stack, nil
    83  	}
    84  	newStack := u.stack.Clone()
    85  	newStack.Status = &status
    86  	u.statuses <- newStack
    87  	return newStack, nil
    88  }
    89  
    90  func (u *testResourceUpdater) deleteSecretsAndConfigMaps() error {
    91  	u.deleteChildResources <- struct{}{}
    92  	return nil
    93  }
    94  
    95  type reconciliationTestCaseResult struct {
    96  	diffs                  []*diff.StackStateDiff
    97  	statuses               []*latest.Stack
    98  	childResourceDeletions int
    99  }
   100  
   101  func runReconcilierTestCase(originalStack *latest.Stack, defaultServiceType coretypes.ServiceType, operation func(*StackReconciler),
   102  	originalState ...interface{}) (reconciliationTestCaseResult, error) {
   103  	cache := &dummyOwnerCache{
   104  		data: make(map[string]stackOwnerCacheEntry),
   105  	}
   106  	childrenStore, err := newTestChildrenStore(originalState...)
   107  	if err != nil {
   108  		return reconciliationTestCaseResult{}, err
   109  	}
   110  	stackStore := newTestStackStore(originalStack)
   111  	chDiffs := make(chan *diff.StackStateDiff)
   112  	chStatusUpdates := make(chan *latest.Stack)
   113  	chChildrenDeletions := make(chan struct{})
   114  	var (
   115  		wg                        sync.WaitGroup
   116  		producedDiffs             []*diff.StackStateDiff
   117  		producedStatusUpdates     []*latest.Stack
   118  		childrenResourceDeletions int
   119  	)
   120  	wg.Add(3)
   121  	go func() {
   122  		defer wg.Done()
   123  		for d := range chDiffs {
   124  			producedDiffs = append(producedDiffs, d)
   125  		}
   126  	}()
   127  	go func() {
   128  		defer wg.Done()
   129  		for s := range chStatusUpdates {
   130  			producedStatusUpdates = append(producedStatusUpdates, s)
   131  		}
   132  	}()
   133  	go func() {
   134  		defer wg.Done()
   135  		for range chChildrenDeletions {
   136  			childrenResourceDeletions++
   137  		}
   138  	}()
   139  	resourceUpdater := newTestResourceUpdaterProvider(chDiffs, chStatusUpdates, chChildrenDeletions)
   140  	testee, err := NewStackReconciler(stackStore, childrenStore, defaultServiceType, resourceUpdater, cache)
   141  	if err != nil {
   142  		close(chDiffs)
   143  		close(chStatusUpdates)
   144  		close(chChildrenDeletions)
   145  		return reconciliationTestCaseResult{}, err
   146  	}
   147  
   148  	operation(testee)
   149  
   150  	close(chDiffs)
   151  	close(chStatusUpdates)
   152  	close(chChildrenDeletions)
   153  	wg.Wait()
   154  	return reconciliationTestCaseResult{childResourceDeletions: childrenResourceDeletions, diffs: producedDiffs, statuses: producedStatusUpdates}, nil
   155  }
   156  
   157  func runReconciliationTestCase(originalStack *latest.Stack, defaultServiceType coretypes.ServiceType,
   158  	originalState ...interface{}) (reconciliationTestCaseResult, error) {
   159  	return runReconcilierTestCase(originalStack, defaultServiceType, func(testee *StackReconciler) {
   160  		testee.reconcileStack(stackresources.ObjKey(originalStack.Namespace, originalStack.Name))
   161  	}, originalState...)
   162  }
   163  
   164  func runRemoveStackTestCase(originalStack *latest.Stack, defaultServiceType coretypes.ServiceType,
   165  	originalState ...interface{}) (reconciliationTestCaseResult, error) {
   166  	return runReconcilierTestCase(originalStack, defaultServiceType, func(testee *StackReconciler) {
   167  		testee.deleteStackChildren(originalStack)
   168  	}, originalState...)
   169  }
   170  
   171  func TestRemoveChildren(t *testing.T) {
   172  	originalStack := Stack("test", WithNamespace("test"))
   173  	svc := Service(originalStack, "svc")
   174  	dep := Deployment(originalStack, "dep")
   175  	daemon := Daemonset(originalStack, "daemon")
   176  	stateful := Statefulset(originalStack, "stateful")
   177  	result, err := runRemoveStackTestCase(originalStack, coretypes.ServiceTypeLoadBalancer, svc, dep, daemon, stateful)
   178  	assert.NoError(t, err)
   179  	assert.Equal(t, 0, len(result.statuses))
   180  	assert.Equal(t, 1, len(result.diffs))
   181  	assert.Equal(t, 0, len(result.diffs[0].DaemonsetsToAdd))
   182  	assert.Equal(t, 0, len(result.diffs[0].DaemonsetsToUpdate))
   183  	assert.Equal(t, 0, len(result.diffs[0].DeploymentsToAdd))
   184  	assert.Equal(t, 0, len(result.diffs[0].DeploymentsToUpdate))
   185  	assert.Equal(t, 0, len(result.diffs[0].ServicesToAdd))
   186  	assert.Equal(t, 0, len(result.diffs[0].ServicesToUpdate))
   187  	assert.Equal(t, 0, len(result.diffs[0].StatefulsetsToAdd))
   188  	assert.Equal(t, 0, len(result.diffs[0].StatefulsetsToUpdate))
   189  	assert.Equal(t, 1, len(result.diffs[0].DaemonsetsToDelete))
   190  	assert.Equal(t, 1, len(result.diffs[0].DeploymentsToDelete))
   191  	assert.Equal(t, 1, len(result.diffs[0].ServicesToDelete))
   192  	assert.Equal(t, 1, len(result.diffs[0].StatefulsetsToDelete))
   193  }
   194  
   195  func TestCreate(t *testing.T) {
   196  	originalStack := Stack("test",
   197  		WithNamespace("test"),
   198  		WithService("dep",
   199  			Image("nginx")),
   200  		WithService("daemon",
   201  			Image("nginx"),
   202  			Deploy(ModeGlobal),
   203  		),
   204  		WithService("stateful",
   205  			Image("nginx"),
   206  			WithVolume(Volume, Source("volumename"), Target("/data")),
   207  		),
   208  	)
   209  	result, err := runReconciliationTestCase(originalStack, coretypes.ServiceTypeLoadBalancer)
   210  	assert.NoError(t, err)
   211  	assert.Equal(t, 1, len(result.statuses))
   212  	assert.Equal(t, statusProgressing(), *result.statuses[0].Status)
   213  	assert.Equal(t, 1, len(result.diffs))
   214  	assert.Equal(t, 1, len(result.diffs[0].DaemonsetsToAdd))
   215  	assert.Equal(t, 0, len(result.diffs[0].DaemonsetsToUpdate))
   216  	assert.Equal(t, 0, len(result.diffs[0].DaemonsetsToDelete))
   217  	assert.Equal(t, 1, len(result.diffs[0].DeploymentsToAdd))
   218  	assert.Equal(t, 0, len(result.diffs[0].DeploymentsToUpdate))
   219  	assert.Equal(t, 0, len(result.diffs[0].DeploymentsToDelete))
   220  	assert.Equal(t, 3, len(result.diffs[0].ServicesToAdd))
   221  	assert.Equal(t, 0, len(result.diffs[0].ServicesToUpdate))
   222  	assert.Equal(t, 0, len(result.diffs[0].ServicesToDelete))
   223  	assert.Equal(t, 1, len(result.diffs[0].StatefulsetsToAdd))
   224  	assert.Equal(t, 0, len(result.diffs[0].StatefulsetsToUpdate))
   225  	assert.Equal(t, 0, len(result.diffs[0].StatefulsetsToDelete))
   226  
   227  	daemon := &result.diffs[0].DaemonsetsToAdd[0]
   228  	deployment := &result.diffs[0].DeploymentsToAdd[0]
   229  	svc0 := &result.diffs[0].ServicesToAdd[0]
   230  	svc1 := &result.diffs[0].ServicesToAdd[1]
   231  	svc2 := &result.diffs[0].ServicesToAdd[2]
   232  	statefulset := &result.diffs[0].StatefulsetsToAdd[0]
   233  
   234  	// ensure owner refs populated
   235  	assert.Equal(t, 1, len(daemon.OwnerReferences))
   236  	assert.Equal(t, 1, len(deployment.OwnerReferences))
   237  	assert.Equal(t, 1, len(svc0.OwnerReferences))
   238  	assert.Equal(t, 1, len(svc1.OwnerReferences))
   239  	assert.Equal(t, 1, len(svc2.OwnerReferences))
   240  	assert.Equal(t, 1, len(statefulset.OwnerReferences))
   241  
   242  	stack := result.statuses[0]
   243  
   244  	// make sure re-reconcile does nothing
   245  	result, err = runReconciliationTestCase(stack, coretypes.ServiceTypeLoadBalancer,
   246  		daemon,
   247  		deployment,
   248  		svc0,
   249  		svc1,
   250  		svc2,
   251  		statefulset)
   252  
   253  	assert.NoError(t, err)
   254  	assert.Equal(t, 0, len(result.statuses))
   255  	assert.Equal(t, 1, len(result.diffs))
   256  	assert.True(t, result.diffs[0].Empty())
   257  
   258  	// update resources status, to simulate readyness
   259  	daemon.Status.NumberUnavailable = 0
   260  	deployment.Status.ReadyReplicas = 1
   261  	statefulset.Status.ReadyReplicas = 1
   262  	result, err = runReconciliationTestCase(stack, coretypes.ServiceTypeLoadBalancer,
   263  		daemon,
   264  		deployment,
   265  		svc0,
   266  		svc1,
   267  		svc2,
   268  		statefulset)
   269  
   270  	assert.NoError(t, err)
   271  	assert.Equal(t, 1, len(result.statuses))
   272  	assert.Equal(t, statusAvailable(), *result.statuses[0].Status)
   273  	assert.Equal(t, 1, len(result.diffs))
   274  	assert.True(t, result.diffs[0].Empty())
   275  }
   276  
   277  func TestPendingDeletionStack(t *testing.T) {
   278  	originalStack := Stack("test",
   279  		WithNamespace("test"),
   280  		WithService("dep",
   281  			Image("nginx")),
   282  		WithService("daemon",
   283  			Image("nginx"),
   284  			Deploy(ModeGlobal),
   285  		),
   286  		WithService("stateful",
   287  			Image("nginx"),
   288  			WithVolume(Volume, Source("volumename"), Target("/data")),
   289  		),
   290  	)
   291  	deleteTS := metav1.Now()
   292  	originalStack.DeletionTimestamp = &deleteTS
   293  	result, err := runReconciliationTestCase(originalStack, coretypes.ServiceTypeLoadBalancer)
   294  	assert.NoError(t, err)
   295  	assert.Equal(t, 0, len(result.statuses))
   296  	assert.Equal(t, 1, len(result.diffs))
   297  	assert.True(t, result.diffs[0].Empty())
   298  	assert.Equal(t, 1, result.childResourceDeletions)
   299  }
   300  
   301  func TestReplayLogs(t *testing.T) {
   302  	cases := []struct {
   303  		fileName           string
   304  		defaultServicetype coretypes.ServiceType
   305  	}{
   306  		{
   307  			fileName:           "d4d-words.json",
   308  			defaultServicetype: coretypes.ServiceTypeLoadBalancer,
   309  		},
   310  		{
   311  			fileName:           "d4d-words-with-unexpected-webhook.json",
   312  			defaultServicetype: coretypes.ServiceTypeLoadBalancer,
   313  		},
   314  		{
   315  			fileName:           "d4d-with-statefulset.json",
   316  			defaultServicetype: coretypes.ServiceTypeLoadBalancer,
   317  		},
   318  		{
   319  			fileName:           "test-ucp-no-dtr.json",
   320  			defaultServicetype: coretypes.ServiceTypeNodePort,
   321  		},
   322  		{
   323  			fileName:           "test-ucp-dtr-with-dct.json",
   324  			defaultServicetype: coretypes.ServiceTypeNodePort,
   325  		},
   326  		{
   327  			fileName:           "test-various-port-ordering.json",
   328  			defaultServicetype: coretypes.ServiceTypeLoadBalancer,
   329  		},
   330  	}
   331  
   332  	for _, c := range cases {
   333  		t.Run(c.fileName, func(t *testing.T) {
   334  			data, err := ioutil.ReadFile(filepath.Join("testcases", c.fileName))
   335  			assert.NoError(t, err)
   336  			var tc TestCase
   337  			assert.NoError(t, json.Unmarshal(data, &tc))
   338  			result, err := runReconciliationTestCase(tc.Stack, c.defaultServicetype, tc.Children.FlattenResources()...)
   339  			assert.NoError(t, err)
   340  			assert.Equal(t, 0, len(result.statuses))
   341  			assert.Equal(t, 1, len(result.diffs))
   342  			assert.True(t, result.diffs[0].Empty())
   343  			if !result.diffs[0].Empty() {
   344  				for _, res := range result.diffs[0].DaemonsetsToUpdate {
   345  					fmt.Printf("daemonset %v changed:\n", res.Name)
   346  					fmt.Println("current:")
   347  					current := tc.Children.Daemonsets[stackresources.ObjKey(res.Namespace, res.Name)]
   348  					data, _ := json.MarshalIndent(&current.Spec, "", "  ")
   349  					fmt.Println(string(data))
   350  					fmt.Println("desired:")
   351  					data, _ = json.MarshalIndent(&res.Spec, "", "  ")
   352  					fmt.Println(string(data))
   353  				}
   354  				for _, res := range result.diffs[0].DeploymentsToUpdate {
   355  					fmt.Printf("deployment %v changed:\n", res.Name)
   356  					fmt.Println("current:")
   357  					current := tc.Children.Deployments[stackresources.ObjKey(res.Namespace, res.Name)]
   358  					data, _ := json.MarshalIndent(&current.Spec, "", "  ")
   359  					fmt.Println(string(data))
   360  					fmt.Println("desired:")
   361  					data, _ = json.MarshalIndent(&res.Spec, "", "  ")
   362  					fmt.Println(string(data))
   363  				}
   364  				for _, res := range result.diffs[0].StatefulsetsToUpdate {
   365  					fmt.Printf("statefulset %v changed:\n", res.Name)
   366  					fmt.Println("current:")
   367  					current := tc.Children.Statefulsets[stackresources.ObjKey(res.Namespace, res.Name)]
   368  					data, _ := json.MarshalIndent(&current.Spec, "", "  ")
   369  					fmt.Println(string(data))
   370  					fmt.Println("desired:")
   371  					data, _ = json.MarshalIndent(&res.Spec, "", "  ")
   372  					fmt.Println(string(data))
   373  				}
   374  				for _, res := range result.diffs[0].ServicesToUpdate {
   375  					fmt.Printf("service %v changed:\n", res.Name)
   376  					fmt.Println("current:")
   377  					current := tc.Children.Services[stackresources.ObjKey(res.Namespace, res.Name)]
   378  					data, _ := json.MarshalIndent(&current.Spec, "", "  ")
   379  					fmt.Println(string(data))
   380  					fmt.Println("desired:")
   381  					data, _ = json.MarshalIndent(&res.Spec, "", "  ")
   382  					fmt.Println(string(data))
   383  				}
   384  			}
   385  		})
   386  	}
   387  }