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(¤t.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(¤t.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(¤t.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(¤t.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 }