github.com/docker/compose-on-kubernetes@v0.5.0/internal/stackresources/diff/stackstatediff_test.go (about)

     1  package diff
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/docker/compose-on-kubernetes/internal/stackresources"
     7  	"github.com/stretchr/testify/assert"
     8  	appstypes "k8s.io/api/apps/v1"
     9  	coretypes "k8s.io/api/core/v1"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  )
    12  
    13  type resourceOpt func(interface{})
    14  
    15  func testDeployment(spec string, labels map[string]string, opts ...resourceOpt) *appstypes.Deployment {
    16  	v := &appstypes.Deployment{
    17  		ObjectMeta: metav1.ObjectMeta{
    18  			Namespace: "ns",
    19  			Name:      "test",
    20  			Labels:    labels,
    21  		},
    22  		Spec: appstypes.DeploymentSpec{
    23  			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"test": spec}},
    24  		},
    25  	}
    26  	for _, o := range opts {
    27  		o(v)
    28  	}
    29  	return v
    30  }
    31  func testStatefulset(spec string, labels map[string]string, opts ...resourceOpt) *appstypes.StatefulSet {
    32  	v := &appstypes.StatefulSet{
    33  		ObjectMeta: metav1.ObjectMeta{
    34  			Namespace: "ns",
    35  			Name:      "test",
    36  			Labels:    labels,
    37  		},
    38  		Spec: appstypes.StatefulSetSpec{
    39  			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"test": spec}},
    40  		},
    41  	}
    42  	for _, o := range opts {
    43  		o(v)
    44  	}
    45  	return v
    46  }
    47  func testDaemonset(spec string, labels map[string]string, opts ...resourceOpt) *appstypes.DaemonSet {
    48  	v := &appstypes.DaemonSet{
    49  		ObjectMeta: metav1.ObjectMeta{
    50  			Namespace: "ns",
    51  			Name:      "test",
    52  			Labels:    labels,
    53  		},
    54  		Spec: appstypes.DaemonSetSpec{
    55  			Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"test": spec}},
    56  		},
    57  	}
    58  	for _, o := range opts {
    59  		o(v)
    60  	}
    61  	return v
    62  }
    63  func testService(spec string, labels map[string]string, opts ...resourceOpt) *coretypes.Service {
    64  	v := &coretypes.Service{
    65  		ObjectMeta: metav1.ObjectMeta{
    66  			Namespace: "ns",
    67  			Name:      "test",
    68  			Labels:    labels,
    69  		},
    70  		Spec: coretypes.ServiceSpec{
    71  			Selector: map[string]string{"test": spec},
    72  		},
    73  	}
    74  	for _, o := range opts {
    75  		o(v)
    76  	}
    77  	return v
    78  }
    79  
    80  func testServiceType(t coretypes.ServiceType) resourceOpt {
    81  	return func(v interface{}) {
    82  		svc := v.(*coretypes.Service)
    83  		svc.Spec.Type = t
    84  	}
    85  }
    86  
    87  func testClusterIP(ip string) resourceOpt {
    88  	return func(v interface{}) {
    89  		svc := v.(*coretypes.Service)
    90  		svc.Spec.ClusterIP = ip
    91  	}
    92  }
    93  
    94  func testPodSpecImage(spec *coretypes.PodTemplateSpec, img string) {
    95  	spec.Spec.InitContainers = []coretypes.Container{
    96  		{
    97  			Image: img,
    98  		},
    99  	}
   100  	spec.Spec.Containers = []coretypes.Container{
   101  		{
   102  			Image: img,
   103  		},
   104  	}
   105  }
   106  func testPodSpecUCPTolerations(spec *coretypes.PodTemplateSpec) {
   107  	spec.Spec.Tolerations = []coretypes.Toleration{
   108  		{
   109  			Key:      "com.docker.ucp.orchestrator.kubernetes",
   110  			Operator: coretypes.TolerationOpExists,
   111  		},
   112  		{
   113  			Key:      "com.docker.ucp.manager",
   114  			Operator: coretypes.TolerationOpExists,
   115  		},
   116  	}
   117  }
   118  func withImage(img string) resourceOpt {
   119  	return func(res interface{}) {
   120  		switch v := res.(type) {
   121  		case *appstypes.Deployment:
   122  			testPodSpecImage(&v.Spec.Template, img)
   123  		case *appstypes.StatefulSet:
   124  			testPodSpecImage(&v.Spec.Template, img)
   125  		case *appstypes.DaemonSet:
   126  			testPodSpecImage(&v.Spec.Template, img)
   127  		}
   128  	}
   129  }
   130  
   131  func withUcpTolerations() resourceOpt {
   132  	return func(res interface{}) {
   133  		switch v := res.(type) {
   134  		case *appstypes.Deployment:
   135  			testPodSpecUCPTolerations(&v.Spec.Template)
   136  		case *appstypes.StatefulSet:
   137  			testPodSpecUCPTolerations(&v.Spec.Template)
   138  		case *appstypes.DaemonSet:
   139  			testPodSpecUCPTolerations(&v.Spec.Template)
   140  		}
   141  	}
   142  }
   143  
   144  func newStackStateOrPanic(objects ...interface{}) *stackresources.StackState {
   145  	res, err := stackresources.NewStackState(objects...)
   146  	if err != nil {
   147  		panic(err)
   148  	}
   149  	return res
   150  }
   151  
   152  func TestStackDiff(t *testing.T) {
   153  	cases := []struct {
   154  		name     string
   155  		current  *stackresources.StackState
   156  		desired  *stackresources.StackState
   157  		expected *StackStateDiff
   158  	}{
   159  		{
   160  			name:     "EmptyToEmpty",
   161  			current:  newStackStateOrPanic(),
   162  			desired:  newStackStateOrPanic(),
   163  			expected: &StackStateDiff{},
   164  		},
   165  		{
   166  			name:    "EmptyToNonEmpty",
   167  			current: newStackStateOrPanic(),
   168  			desired: newStackStateOrPanic(
   169  				testDeployment("spec", nil),
   170  				testStatefulset("spec", nil),
   171  				testDaemonset("spec", nil),
   172  				testService("spec", nil),
   173  			),
   174  			expected: &StackStateDiff{
   175  				DeploymentsToAdd: []appstypes.Deployment{
   176  					*testDeployment("spec", nil),
   177  				},
   178  				StatefulsetsToAdd: []appstypes.StatefulSet{
   179  					*testStatefulset("spec", nil),
   180  				},
   181  				DaemonsetsToAdd: []appstypes.DaemonSet{
   182  					*testDaemonset("spec", nil),
   183  				},
   184  				ServicesToAdd: []coretypes.Service{
   185  					*testService("spec", nil),
   186  				},
   187  			},
   188  		}, {
   189  			name:    "NonEmptyToEmpty",
   190  			desired: newStackStateOrPanic(),
   191  			current: newStackStateOrPanic(
   192  				testDeployment("spec", nil),
   193  				testStatefulset("spec", nil),
   194  				testDaemonset("spec", nil),
   195  				testService("spec", nil),
   196  			),
   197  			expected: &StackStateDiff{
   198  				DeploymentsToDelete: []appstypes.Deployment{
   199  					*testDeployment("spec", nil),
   200  				},
   201  				StatefulsetsToDelete: []appstypes.StatefulSet{
   202  					*testStatefulset("spec", nil),
   203  				},
   204  				DaemonsetsToDelete: []appstypes.DaemonSet{
   205  					*testDaemonset("spec", nil),
   206  				},
   207  				ServicesToDelete: []coretypes.Service{
   208  					*testService("spec", nil),
   209  				},
   210  			},
   211  		}, {
   212  			name: "UpdateSpec",
   213  			current: newStackStateOrPanic(
   214  				testDeployment("specold", nil),
   215  				testStatefulset("specold", nil),
   216  				testDaemonset("specold", nil),
   217  				testService("specold", nil),
   218  			),
   219  			desired: newStackStateOrPanic(
   220  				testDeployment("spec", nil),
   221  				testStatefulset("spec", nil),
   222  				testDaemonset("spec", nil),
   223  				testService("spec", nil),
   224  			),
   225  			expected: &StackStateDiff{
   226  				DeploymentsToUpdate: []appstypes.Deployment{
   227  					*testDeployment("spec", nil),
   228  				},
   229  				StatefulsetsToUpdate: []appstypes.StatefulSet{
   230  					*testStatefulset("spec", nil),
   231  				},
   232  				DaemonsetsToUpdate: []appstypes.DaemonSet{
   233  					*testDaemonset("spec", nil),
   234  				},
   235  				ServicesToUpdate: []coretypes.Service{
   236  					*testService("spec", nil),
   237  				},
   238  			},
   239  		},
   240  
   241  		{
   242  			name: "Same",
   243  			current: newStackStateOrPanic(
   244  				testDeployment("spec", map[string]string{"key": "val"}),
   245  				testStatefulset("spec", map[string]string{"key": "val"}),
   246  				testDaemonset("spec", map[string]string{"key": "val"}),
   247  				testService("spec", map[string]string{"key": "val"}),
   248  			),
   249  			desired: newStackStateOrPanic(
   250  				testDeployment("spec", map[string]string{"key": "val"}),
   251  				testStatefulset("spec", map[string]string{"key": "val"}),
   252  				testDaemonset("spec", map[string]string{"key": "val"}),
   253  				testService("spec", map[string]string{"key": "val"}),
   254  			),
   255  			expected: &StackStateDiff{},
   256  		},
   257  		{
   258  			name: "DCT-Image-Patching",
   259  			current: newStackStateOrPanic(
   260  				testDeployment("spec", map[string]string{"key": "val"}, withImage("testimg:testtag@test-sha")),
   261  				testStatefulset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag@test-sha")),
   262  				testDaemonset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag@test-sha")),
   263  				testService("spec", map[string]string{"key": "val"}),
   264  			),
   265  			desired: newStackStateOrPanic(
   266  				testDeployment("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   267  				testStatefulset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   268  				testDaemonset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   269  				testService("spec", map[string]string{"key": "val"}),
   270  			),
   271  			expected: &StackStateDiff{},
   272  		},
   273  		{
   274  			name: "image-tag-update",
   275  			current: newStackStateOrPanic(
   276  				testDeployment("spec", map[string]string{"key": "val"}, withImage("testimg:oldtag")),
   277  				testStatefulset("spec", map[string]string{"key": "val"}, withImage("testimg:oldtag")),
   278  				testDaemonset("spec", map[string]string{"key": "val"}, withImage("testimg:oldtag")),
   279  				testService("spec", map[string]string{"key": "val"}),
   280  			),
   281  			desired: newStackStateOrPanic(
   282  				testDeployment("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   283  				testStatefulset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   284  				testDaemonset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   285  				testService("spec", map[string]string{"key": "val"}),
   286  			),
   287  			expected: &StackStateDiff{
   288  				DeploymentsToUpdate: []appstypes.Deployment{
   289  					*testDeployment("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   290  				},
   291  				StatefulsetsToUpdate: []appstypes.StatefulSet{
   292  					*testStatefulset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   293  				},
   294  				DaemonsetsToUpdate: []appstypes.DaemonSet{
   295  					*testDaemonset("spec", map[string]string{"key": "val"}, withImage("testimg:testtag")),
   296  				},
   297  			},
   298  		},
   299  		{
   300  			name: "UCP-Tolerations",
   301  			current: newStackStateOrPanic(
   302  				testDeployment("spec", map[string]string{"key": "val"}, withUcpTolerations()),
   303  				testStatefulset("spec", map[string]string{"key": "val"}, withUcpTolerations()),
   304  				testDaemonset("spec", map[string]string{"key": "val"}, withUcpTolerations()),
   305  				testService("spec", map[string]string{"key": "val"}),
   306  			),
   307  			desired: newStackStateOrPanic(
   308  				testDeployment("spec", map[string]string{"key": "val"}),
   309  				testStatefulset("spec", map[string]string{"key": "val"}),
   310  				testDaemonset("spec", map[string]string{"key": "val"}),
   311  				testService("spec", map[string]string{"key": "val"}),
   312  			),
   313  			expected: &StackStateDiff{},
   314  		},
   315  		{
   316  			name: "labels-changes",
   317  			current: newStackStateOrPanic(
   318  				testDeployment("spec", map[string]string{"key": "val"}),
   319  				testStatefulset("spec", map[string]string{"key": "val"}),
   320  				testDaemonset("spec", map[string]string{"key": "val"}),
   321  				testService("spec", map[string]string{"key": "val"}),
   322  			),
   323  			desired: newStackStateOrPanic(
   324  				testDeployment("spec", map[string]string{"key": "val2"}),
   325  				testStatefulset("spec", map[string]string{"key": "val2"}),
   326  				testDaemonset("spec", map[string]string{"key": "val2"}),
   327  				testService("spec", map[string]string{"key": "val2"}),
   328  			),
   329  			expected: &StackStateDiff{
   330  				DeploymentsToUpdate: []appstypes.Deployment{
   331  					*testDeployment("spec", map[string]string{"key": "val2"}),
   332  				},
   333  				StatefulsetsToUpdate: []appstypes.StatefulSet{
   334  					*testStatefulset("spec", map[string]string{"key": "val2"}),
   335  				},
   336  				DaemonsetsToUpdate: []appstypes.DaemonSet{
   337  					*testDaemonset("spec", map[string]string{"key": "val2"}),
   338  				},
   339  				ServicesToUpdate: []coretypes.Service{
   340  					*testService("spec", map[string]string{"key": "val2"}),
   341  				},
   342  			},
   343  		},
   344  		{
   345  			name: "service-headless-to-cluster-ip",
   346  			current: newStackStateOrPanic(
   347  				testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP(coretypes.ClusterIPNone)),
   348  			),
   349  			desired: newStackStateOrPanic(
   350  				testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP("")),
   351  			),
   352  			expected: &StackStateDiff{
   353  				ServicesToDelete: []coretypes.Service{
   354  					*testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP(coretypes.ClusterIPNone)),
   355  				},
   356  				ServicesToAdd: []coretypes.Service{
   357  					*testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP("")),
   358  				},
   359  			},
   360  		},
   361  		{
   362  			name: "service-cluster-ip-to-headless",
   363  			current: newStackStateOrPanic(
   364  				testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP("1.2.3.4")),
   365  			),
   366  			desired: newStackStateOrPanic(
   367  				testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP(coretypes.ClusterIPNone)),
   368  			),
   369  			expected: &StackStateDiff{
   370  				ServicesToDelete: []coretypes.Service{
   371  					*testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP("1.2.3.4")),
   372  				},
   373  				ServicesToAdd: []coretypes.Service{
   374  					*testService("spec", nil, testServiceType(coretypes.ServiceTypeClusterIP), testClusterIP(coretypes.ClusterIPNone)),
   375  				},
   376  			},
   377  		},
   378  	}
   379  
   380  	for _, c := range cases {
   381  		t.Run(c.name, func(t *testing.T) {
   382  			assert.Equal(t, c.expected, ComputeDiff(c.current, c.desired))
   383  		})
   384  	}
   385  }