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  }