github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/manager/controlapi/resource_test.go (about) 1 package controlapi 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 gogotypes "github.com/gogo/protobuf/types" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 "google.golang.org/grpc/codes" 12 13 "github.com/docker/swarmkit/api" 14 "github.com/docker/swarmkit/manager/state/store" 15 "github.com/docker/swarmkit/testutils" 16 ) 17 18 const testExtName = "testExtension" 19 20 func prepResource(t *testing.T, ts *testServer) *api.Resource { 21 extensionReq := &api.CreateExtensionRequest{ 22 Annotations: &api.Annotations{ 23 Name: testExtName, 24 }, 25 } 26 // create an extension so we can create valid requests 27 extResp, err := ts.Client.CreateExtension(context.Background(), extensionReq) 28 assert.NoError(t, err) 29 assert.NotNil(t, extResp) 30 assert.NotNil(t, extResp.Extension) 31 32 // We need to stuff some arbitrary data into an Any for this, so let's 33 // create a simple Annnotations object 34 anyContent := &api.Annotations{ 35 Name: "SomeName", 36 Labels: map[string]string{"some": "label"}, 37 } 38 anyMsg, err := gogotypes.MarshalAny(anyContent) 39 require.NoError(t, err) 40 41 // first, create a valid resource, to ensure that that works 42 req := &api.CreateResourceRequest{ 43 Annotations: &api.Annotations{ 44 Name: "ValidResource", 45 }, 46 Kind: testExtName, 47 Payload: anyMsg, 48 } 49 50 createResp, err := ts.Client.CreateResource(context.Background(), req) 51 require.NoError(t, err) 52 require.NotNil(t, createResp) 53 require.NotNil(t, createResp.Resource) 54 return createResp.Resource 55 } 56 57 func TestCreateResource(t *testing.T) { 58 ts := newTestServer(t) 59 defer ts.Stop() 60 61 extensionReq := &api.CreateExtensionRequest{ 62 Annotations: &api.Annotations{ 63 Name: testExtName, 64 }, 65 } 66 67 // create an extension so we can create valid requests 68 resp, err := ts.Client.CreateExtension(context.Background(), extensionReq) 69 assert.NoError(t, err) 70 assert.NotNil(t, resp) 71 assert.NotNil(t, resp.Extension) 72 73 // first, create a valid resource, to ensure that that works 74 t.Run("ValidResource", func(t *testing.T) { 75 req := &api.CreateResourceRequest{ 76 Annotations: &api.Annotations{ 77 Name: "ValidResource", 78 }, 79 Kind: testExtName, 80 Payload: &gogotypes.Any{}, 81 } 82 83 resp, err := ts.Client.CreateResource(context.Background(), req) 84 assert.NoError(t, err) 85 assert.NotNil(t, resp) 86 assert.NotNil(t, resp.Resource) 87 assert.NotEmpty(t, resp.Resource.ID) 88 assert.NotZero(t, resp.Resource.Meta.Version.Index) 89 }) 90 91 // then, test all the error cases. 92 for _, tc := range []struct { 93 name string 94 req *api.CreateResourceRequest 95 code codes.Code 96 }{ 97 { 98 name: "NilAnnotations", 99 req: &api.CreateResourceRequest{}, 100 code: codes.InvalidArgument, 101 }, { 102 name: "EmptyName", 103 req: &api.CreateResourceRequest{ 104 Annotations: &api.Annotations{ 105 // my gut says include a label in this test 106 Labels: map[string]string{"name": "nope"}, 107 }, 108 Kind: testExtName, 109 }, 110 code: codes.InvalidArgument, 111 }, { 112 name: "EmptyKind", 113 req: &api.CreateResourceRequest{ 114 Annotations: &api.Annotations{ 115 Name: "EmptyKind", 116 }, 117 }, 118 code: codes.InvalidArgument, 119 }, { 120 name: "InvalidKind", 121 req: &api.CreateResourceRequest{ 122 Annotations: &api.Annotations{ 123 Name: "InvalidKind", 124 }, 125 Kind: "notARealKind", 126 }, 127 code: codes.InvalidArgument, 128 }, { 129 name: "NameConflict", 130 req: &api.CreateResourceRequest{ 131 Annotations: &api.Annotations{ 132 Name: "ValidResource", 133 }, 134 Kind: testExtName, 135 Payload: &gogotypes.Any{}, 136 }, 137 code: codes.AlreadyExists, 138 }, 139 } { 140 t.Run(tc.name, func(t *testing.T) { 141 _, err := ts.Client.CreateResource(context.Background(), tc.req) 142 assert.Error(t, err) 143 assert.Equal(t, tc.code, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 144 }) 145 } 146 } 147 148 func TestUpdateResourceValid(t *testing.T) { 149 ts := newTestServer(t) 150 defer ts.Stop() 151 152 resource := prepResource(t, ts) 153 resourceID := resource.ID 154 155 for _, tc := range []struct { 156 name string 157 // transform mutates the resource to the expected form, and generates a 158 // request for it 159 transform func(*testing.T, *api.Resource) *api.UpdateResourceRequest 160 }{ 161 { 162 name: "Both", 163 transform: func(t *testing.T, r *api.Resource) *api.UpdateResourceRequest { 164 newAnnotations := &api.Annotations{ 165 Name: "ValidResource", 166 Labels: map[string]string{"some": "newlabels"}, 167 } 168 newContent := &api.Annotations{ 169 Name: "SomeNewName", 170 } 171 newMsg, err := gogotypes.MarshalAny(newContent) 172 require.NoError(t, err) 173 r.Annotations = *newAnnotations 174 r.Payload = newMsg 175 176 return &api.UpdateResourceRequest{ 177 ResourceID: resourceID, 178 ResourceVersion: &r.Meta.Version, 179 Annotations: newAnnotations, 180 Payload: newMsg, 181 } 182 }, 183 }, { 184 name: "OnlyAnnotations", 185 transform: func(t *testing.T, r *api.Resource) *api.UpdateResourceRequest { 186 r.Annotations.Labels = map[string]string{"onlyUpdating": "theseLabels"} 187 return &api.UpdateResourceRequest{ 188 ResourceID: r.ID, 189 ResourceVersion: &r.Meta.Version, 190 Annotations: &r.Annotations, 191 } 192 }, 193 }, { 194 name: "OnlyPayload", 195 transform: func(t *testing.T, r *api.Resource) *api.UpdateResourceRequest { 196 newContent := &api.Annotations{ 197 Name: "OnlyUpdatingPayload", 198 } 199 newMsg, err := gogotypes.MarshalAny(newContent) 200 require.NoError(t, err) 201 r.Payload = newMsg 202 return &api.UpdateResourceRequest{ 203 ResourceID: resourceID, 204 ResourceVersion: &r.Meta.Version, 205 Payload: newMsg, 206 } 207 }, 208 }, 209 } { 210 t.Run(tc.name, func(t *testing.T) { 211 var r *api.Resource 212 ts.Store.View(func(tx store.ReadTx) { 213 r = store.GetResource(tx, resourceID) 214 }) 215 req := tc.transform(t, r) 216 resp, err := ts.Client.UpdateResource(context.Background(), req) 217 218 require.NoError(t, err) 219 require.NotNil(t, resp) 220 require.NotNil(t, resp.Resource) 221 assert.Equal(t, resp.Resource.Payload, r.Payload) 222 assert.Equal(t, resp.Resource.Annotations, r.Annotations) 223 assert.Equal(t, resp.Resource.Kind, testExtName) 224 }) 225 } 226 } 227 228 func TestUpdateResourceInvalid(t *testing.T) { 229 ts := newTestServer(t) 230 defer ts.Stop() 231 232 resource := prepResource(t, ts) 233 234 for _, tc := range []struct { 235 name string 236 req *api.UpdateResourceRequest 237 code codes.Code 238 }{ 239 { 240 name: "MissingID", 241 req: &api.UpdateResourceRequest{ 242 ResourceID: "", 243 ResourceVersion: &resource.Meta.Version, 244 }, 245 code: codes.InvalidArgument, 246 }, { 247 name: "MissingVersion", 248 req: &api.UpdateResourceRequest{ 249 ResourceID: resource.ID, 250 }, 251 code: codes.InvalidArgument, 252 }, { 253 name: "NotFound", 254 req: &api.UpdateResourceRequest{ 255 ResourceID: "notreal", 256 ResourceVersion: &resource.Meta.Version, 257 }, 258 code: codes.NotFound, 259 }, { 260 name: "IncorrectVersion", 261 req: &api.UpdateResourceRequest{ 262 ResourceID: resource.ID, 263 ResourceVersion: &api.Version{Index: 0}, 264 }, 265 code: codes.InvalidArgument, 266 }, { 267 name: "ChangedName", 268 req: &api.UpdateResourceRequest{ 269 ResourceID: resource.ID, 270 ResourceVersion: &resource.Meta.Version, 271 Annotations: &api.Annotations{ 272 Name: "different", 273 }, 274 }, 275 code: codes.InvalidArgument, 276 }, 277 } { 278 t.Run(tc.name, func(t *testing.T) { 279 _, err := ts.Client.UpdateResource(context.Background(), tc.req) 280 assert.Error(t, err) 281 assert.Equal(t, tc.code, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 282 }) 283 } 284 } 285 286 func TestGetResource(t *testing.T) { 287 ts := newTestServer(t) 288 defer ts.Stop() 289 290 resource := prepResource(t, ts) 291 292 // empty resource ID should fail with invalid argument 293 resp, err := ts.Client.GetResource(context.Background(), &api.GetResourceRequest{}) 294 assert.Error(t, err) 295 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 296 297 // id which does not exist should return NotFound 298 resp, err = ts.Client.GetResource( 299 context.Background(), 300 &api.GetResourceRequest{ResourceID: "notreal"}, 301 ) 302 assert.Error(t, err) 303 assert.Equal(t, codes.NotFound, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 304 305 // ID which exists should return the resource 306 resp, err = ts.Client.GetResource( 307 context.Background(), 308 &api.GetResourceRequest{ResourceID: resource.ID}, 309 ) 310 require.NoError(t, err) 311 require.NotNil(t, resp) 312 require.NotNil(t, resp.Resource) 313 assert.Equal(t, resp.Resource, resource) 314 } 315 316 func TestRemoveResource(t *testing.T) { 317 ts := newTestServer(t) 318 defer ts.Stop() 319 320 resource := prepResource(t, ts) 321 322 // empty resource ID should fail with invalid argument 323 resp, err := ts.Client.RemoveResource(context.Background(), &api.RemoveResourceRequest{}) 324 assert.Error(t, err) 325 assert.Equal(t, codes.InvalidArgument, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 326 assert.Nil(t, resp) 327 328 // id which does not exist should return NotFound 329 resp, err = ts.Client.RemoveResource( 330 context.Background(), 331 &api.RemoveResourceRequest{ResourceID: "notreal"}, 332 ) 333 assert.Error(t, err) 334 assert.Equal(t, codes.NotFound, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 335 assert.Nil(t, resp) 336 337 // ID which exists should return the resource 338 resp, err = ts.Client.RemoveResource( 339 context.Background(), 340 &api.RemoveResourceRequest{ResourceID: resource.ID}, 341 ) 342 require.NoError(t, err) 343 require.NotNil(t, resp) 344 345 // Trying to delete again should return not found 346 resp, err = ts.Client.RemoveResource( 347 context.Background(), 348 &api.RemoveResourceRequest{ResourceID: resource.ID}, 349 ) 350 assert.Error(t, err) 351 assert.Equal(t, codes.NotFound, testutils.ErrorCode(err), testutils.ErrorDesc(err)) 352 assert.Nil(t, resp) 353 } 354 355 func TestListResources(t *testing.T) { 356 ts := newTestServer(t) 357 defer ts.Stop() 358 359 kinds := []string{"kind1", "kind2", "kind3"} 360 for _, kind := range kinds { 361 // create some extensions 362 _, err := ts.Client.CreateExtension( 363 context.Background(), &api.CreateExtensionRequest{ 364 Annotations: &api.Annotations{Name: kind}, 365 }, 366 ) 367 require.NoError(t, err) 368 } 369 370 // everything beyond this point is pretty much copied verbatim from 371 // config_test.go, but changed for resources and to also test kinds 372 373 listResources := func(req *api.ListResourcesRequest) map[string]*api.Resource { 374 resp, err := ts.Client.ListResources(context.Background(), req) 375 require.NoError(t, err) 376 require.NotNil(t, resp) 377 378 byName := make(map[string]*api.Resource) 379 for _, resource := range resp.Resources { 380 byName[resource.Annotations.Name] = resource 381 } 382 return byName 383 } 384 385 // listing when there is nothing returns an empty list but no error 386 result := listResources(&api.ListResourcesRequest{}) 387 assert.Len(t, result, 0) 388 389 // create a bunch of resources to test filtering 390 allListableNames := []string{"aaa", "aab", "abc", "bbb", "bac", "bbc", "ccc", "cac", "cbc", "ddd"} 391 resourceNamesToID := make(map[string]string) 392 for i, resourceName := range allListableNames { 393 resp, err := ts.Client.CreateResource( 394 context.Background(), &api.CreateResourceRequest{ 395 Annotations: &api.Annotations{ 396 Name: resourceName, 397 Labels: map[string]string{ 398 "mod2": fmt.Sprintf("%d", i%2), 399 "mod4": fmt.Sprintf("%d", i%4), 400 }, 401 }, 402 Kind: kinds[i%len(kinds)], 403 }, 404 ) 405 require.NoError(t, err) 406 resourceNamesToID[resourceName] = resp.Resource.ID 407 } 408 409 type listTestCase struct { 410 desc string 411 expected []string 412 filter *api.ListResourcesRequest_Filters 413 } 414 415 listResourceTestCases := []listTestCase{ 416 { 417 desc: "no filter: all available resources are returned", 418 expected: allListableNames, 419 filter: nil, 420 }, { 421 desc: "searching for something that doesn't match returns an empty list", 422 expected: nil, 423 filter: &api.ListResourcesRequest_Filters{Names: []string{"aa"}}, 424 }, { 425 desc: "multiple name filters are or-ed together", 426 expected: []string{"aaa", "bbb", "ccc"}, 427 filter: &api.ListResourcesRequest_Filters{Names: []string{"aaa", "bbb", "ccc"}}, 428 }, { 429 desc: "multiple name prefix filters are or-ed together", 430 expected: []string{"aaa", "aab", "bbb", "bbc"}, 431 filter: &api.ListResourcesRequest_Filters{NamePrefixes: []string{"aa", "bb"}}, 432 }, { 433 desc: "multiple ID prefix filters are or-ed together", 434 expected: []string{"aaa", "bbb"}, 435 filter: &api.ListResourcesRequest_Filters{IDPrefixes: []string{ 436 resourceNamesToID["aaa"], resourceNamesToID["bbb"]}, 437 }, 438 }, { 439 desc: "name prefix, name, and ID prefix filters are or-ed together", 440 expected: []string{"aaa", "aab", "bbb", "bbc", "ccc", "ddd"}, 441 filter: &api.ListResourcesRequest_Filters{ 442 Names: []string{"aaa", "ccc"}, 443 NamePrefixes: []string{"aa", "bb"}, 444 IDPrefixes: []string{resourceNamesToID["aaa"], resourceNamesToID["ddd"]}, 445 }, 446 }, { 447 desc: "all labels in the label map must be matched", 448 expected: []string{allListableNames[0], allListableNames[4], allListableNames[8]}, 449 filter: &api.ListResourcesRequest_Filters{ 450 Labels: map[string]string{ 451 "mod2": "0", 452 "mod4": "0", 453 }, 454 }, 455 }, { 456 desc: "name prefix, name, and ID prefix filters are or-ed together, but the results must match all labels in the label map", 457 // + indicates that these would be selected with the name/id/prefix filtering, and 0/1 at the end indicate the mod2 value: 458 // +"aaa"0, +"aab"1, "abc"0, +"bbb"1, "bac"0, +"bbc"1, +"ccc"0, "cac"1, "cbc"0, +"ddd"1 459 expected: []string{"aaa", "ccc"}, 460 filter: &api.ListResourcesRequest_Filters{ 461 Names: []string{"aaa", "ccc"}, 462 NamePrefixes: []string{"aa", "bb"}, 463 IDPrefixes: []string{resourceNamesToID["aaa"], resourceNamesToID["ddd"]}, 464 Labels: map[string]string{ 465 "mod2": "0", 466 }, 467 }, 468 }, { 469 desc: "extension filter matches only specified extension", 470 expected: []string{ 471 allListableNames[0], allListableNames[3], 472 allListableNames[6], allListableNames[9], 473 }, 474 filter: &api.ListResourcesRequest_Filters{ 475 Kind: kinds[0], 476 }, 477 }, { 478 desc: "name prefix, name, and ID prefix filters are or-ed together, but the results must match all labels in the label map and the extension", 479 expected: []string{"aaa"}, 480 // without label and extension filters, we would have: 481 // expected: []string{"aaa", "aab", "abc", "bbb", "bbc", "ddd"}, 482 // when we mod2, we get left with aaa and abc 483 // when we filter on kinds[0], we're left with just aaa (as abc is 484 // kinds[0]) 485 filter: &api.ListResourcesRequest_Filters{ 486 Names: []string{"aaa", "abc"}, 487 NamePrefixes: []string{"aa", "bb"}, 488 IDPrefixes: []string{resourceNamesToID["aaa"], resourceNamesToID["ddd"]}, 489 Labels: map[string]string{ 490 "mod2": "0", 491 }, 492 Kind: kinds[0], 493 }, 494 }, 495 } 496 497 for _, expectation := range listResourceTestCases { 498 result := listResources(&api.ListResourcesRequest{Filters: expectation.filter}) 499 assert.Len(t, result, len(expectation.expected), expectation.desc) 500 for _, name := range expectation.expected { 501 assert.Contains(t, result, name, expectation.desc) 502 assert.NotNil(t, result[name], expectation.desc) 503 assert.Equal(t, resourceNamesToID[name], result[name].ID, expectation.desc) 504 } 505 } 506 }