func TestGetSecret(t *testing.T) { ts := newTestServer(t) defer ts.Stop() // ---- getting a secret without providing an ID results in an InvalidArgument ---- _, err := ts.Client.GetSecret(context.Background(), &api.GetSecretRequest{}) assert.Error(t, err) assert.Equal(t, codes.InvalidArgument, grpc.Code(err), grpc.ErrorDesc(err)) // ---- getting a non-existent secret fails with NotFound ---- _, err = ts.Client.GetSecret(context.Background(), &api.GetSecretRequest{SecretID: "12345"}) assert.Error(t, err) assert.Equal(t, codes.NotFound, grpc.Code(err), grpc.ErrorDesc(err)) // ---- getting an existing secret returns the secret with all the private data cleaned ---- secret := secretFromSecretSpec(createSecretSpec("name", []byte("data"), nil)) err = ts.Store.Update(func(tx store.Tx) error { return store.CreateSecret(tx, secret) }) assert.NoError(t, err) resp, err := ts.Client.GetSecret(context.Background(), &api.GetSecretRequest{SecretID: secret.ID}) assert.NoError(t, err) assert.NotNil(t, resp) assert.NotNil(t, resp.Secret) // the data should be empty/omitted assert.NotEqual(t, secret, resp.Secret) secret.Spec.Data = nil assert.Equal(t, secret, resp.Secret) }
// CreateSecret creates and return a `CreateSecretResponse` with a `Secret` based // on the provided `CreateSecretRequest.SecretSpec`. // - Returns `InvalidArgument` if the `CreateSecretRequest.SecretSpec` is malformed, // or if the secret data is too long or contains invalid characters. // - Returns an error if the creation fails. func (s *Server) CreateSecret(ctx context.Context, request *api.CreateSecretRequest) (*api.CreateSecretResponse, error) { if err := validateSecretSpec(request.Spec); err != nil { return nil, err } secret := secretFromSecretSpec(request.Spec) // the store will handle name conflicts err := s.store.Update(func(tx store.Tx) error { return store.CreateSecret(tx, secret) }) switch err { case store.ErrNameConflict: return nil, grpc.Errorf(codes.AlreadyExists, "secret %s already exists", request.Spec.Annotations.Name) case nil: secret.Spec.Data = nil // clean the actual secret data so it's never returned log.G(ctx).WithFields(logrus.Fields{ "secret.Name": request.Spec.Annotations.Name, "method": "CreateSecret", }).Debugf("secret created") return &api.CreateSecretResponse{Secret: secret}, nil default: return nil, err } }
func TestRemoveUnusedSecret(t *testing.T) { ts := newTestServer(t) defer ts.Stop() // removing a secret without providing an ID results in an InvalidArgument _, err := ts.Client.RemoveSecret(context.Background(), &api.RemoveSecretRequest{}) assert.Error(t, err) assert.Equal(t, codes.InvalidArgument, grpc.Code(err), grpc.ErrorDesc(err)) // removing a secret that exists succeeds secret := secretFromSecretSpec(createSecretSpec("name", []byte("data"), nil)) err = ts.Store.Update(func(tx store.Tx) error { return store.CreateSecret(tx, secret) }) assert.NoError(t, err) resp, err := ts.Client.RemoveSecret(context.Background(), &api.RemoveSecretRequest{SecretID: secret.ID}) assert.NoError(t, err) assert.Equal(t, api.RemoveSecretResponse{}, *resp) // ---- it was really removed because attempting to remove it again fails with a NotFound ---- _, err = ts.Client.RemoveSecret(context.Background(), &api.RemoveSecretRequest{SecretID: secret.ID}) assert.Error(t, err) assert.Equal(t, codes.NotFound, grpc.Code(err), grpc.ErrorDesc(err)) }
func createSecret(t *testing.T, ts *testServer, secretName, target string) *api.SecretReference { secretSpec := createSecretSpec(secretName, []byte(secretName), nil) secret := &api.Secret{ ID: fmt.Sprintf("ID%v", secretName), Spec: *secretSpec, } err := ts.Store.Update(func(tx store.Tx) error { return store.CreateSecret(tx, secret) }) assert.NoError(t, err) return &api.SecretReference{ SecretName: secret.Spec.Annotations.Name, SecretID: secret.ID, Target: &api.SecretReference_File{ File: &api.SecretReference_FileTarget{ Name: target, UID: "0", GID: "0", Mode: 0666, }, }, } }
func TestListSecrets(t *testing.T) { s := newTestServer(t) listSecrets := func(req *api.ListSecretsRequest) map[string]*api.Secret { resp, err := s.Client.ListSecrets(context.Background(), req) assert.NoError(t, err) assert.NotNil(t, resp) byName := make(map[string]*api.Secret) for _, secret := range resp.Secrets { byName[secret.Spec.Annotations.Name] = secret } return byName } // ---- Listing secrets when there are no secrets returns an empty list but no error ---- result := listSecrets(&api.ListSecretsRequest{}) assert.Len(t, result, 0) // ---- Create a bunch of secrets in the store so we can test filtering ---- allListableNames := []string{"aaa", "aab", "abc", "bbb", "bac", "bbc", "ccc", "cac", "cbc", "ddd"} secretNamesToID := make(map[string]string) for i, secretName := range allListableNames { secret := secretFromSecretSpec(createSecretSpec(secretName, []byte("secret"), map[string]string{ "mod2": fmt.Sprintf("%d", i%2), "mod4": fmt.Sprintf("%d", i%4), })) err := s.Store.Update(func(tx store.Tx) error { return store.CreateSecret(tx, secret) }) assert.NoError(t, err) secretNamesToID[secretName] = secret.ID } // also add an internal secret to show that it's never returned internalSecret := secretFromSecretSpec(createSecretSpec("internal", []byte("secret"), map[string]string{ "mod2": "1", "mod4": "1", })) internalSecret.Internal = true err := s.Store.Update(func(tx store.Tx) error { return store.CreateSecret(tx, internalSecret) }) assert.NoError(t, err) secretNamesToID["internal"] = internalSecret.ID // ---- build up our list of expectations for what secrets get filtered ---- type listTestCase struct { desc string expected []string filter *api.ListSecretsRequest_Filters } listSecretTestCases := []listTestCase{ { desc: "no filter: all the available secrets are returned", expected: allListableNames, filter: nil, }, { desc: "searching for something that doesn't match returns an empty list", expected: nil, filter: &api.ListSecretsRequest_Filters{Names: []string{"aa", "internal"}}, }, { desc: "multiple name filters are or-ed together", expected: []string{"aaa", "bbb", "ccc"}, filter: &api.ListSecretsRequest_Filters{Names: []string{"aaa", "bbb", "ccc", "internal"}}, }, { desc: "multiple name prefix filters are or-ed together", expected: []string{"aaa", "aab", "bbb", "bbc"}, filter: &api.ListSecretsRequest_Filters{NamePrefixes: []string{"aa", "bb", "int"}}, }, { desc: "multiple ID prefix filters are or-ed together", expected: []string{"aaa", "bbb"}, filter: &api.ListSecretsRequest_Filters{IDPrefixes: []string{ secretNamesToID["aaa"], secretNamesToID["bbb"], secretNamesToID["internal"]}, }, }, { desc: "name prefix, name, and ID prefix filters are or-ed together", expected: []string{"aaa", "aab", "bbb", "bbc", "ccc", "ddd"}, filter: &api.ListSecretsRequest_Filters{ Names: []string{"aaa", "ccc", "internal"}, NamePrefixes: []string{"aa", "bb", "int"}, IDPrefixes: []string{secretNamesToID["aaa"], secretNamesToID["ddd"], secretNamesToID["internal"]}, }, }, { desc: "all labels in the label map must be matched", expected: []string{allListableNames[0], allListableNames[4], allListableNames[8]}, filter: &api.ListSecretsRequest_Filters{ Labels: map[string]string{ "mod2": "0", "mod4": "0", }, }, }, { desc: "name prefix, name, and ID prefix filters are or-ed together, but the results must match all labels in the label map", // + indicates that these would be selected with the name/id/prefix filtering, and 0/1 at the end indicate the mod2 value: // +"aaa"0, +"aab"1, "abc"0, +"bbb"1, "bac"0, +"bbc"1, +"ccc"0, "cac"1, "cbc"0, +"ddd"1 expected: []string{"aaa", "ccc"}, filter: &api.ListSecretsRequest_Filters{ Names: []string{"aaa", "ccc", "internal"}, NamePrefixes: []string{"aa", "bb", "int"}, IDPrefixes: []string{secretNamesToID["aaa"], secretNamesToID["ddd"], secretNamesToID["internal"]}, Labels: map[string]string{ "mod2": "0", }, }, }, } // ---- run the filter tests ---- for _, expectation := range listSecretTestCases { result := listSecrets(&api.ListSecretsRequest{Filters: expectation.filter}) assert.Len(t, result, len(expectation.expected), expectation.desc) for _, name := range expectation.expected { assert.Contains(t, result, name, expectation.desc) assert.NotNil(t, result[name], expectation.desc) assert.Equal(t, secretNamesToID[name], result[name].ID, expectation.desc) } } }
func TestUpdateSecret(t *testing.T) { ts := newTestServer(t) defer ts.Stop() // Add a secret to the store to update secret := secretFromSecretSpec(createSecretSpec("name", []byte("data"), map[string]string{"mod2": "0", "mod4": "0"})) err := ts.Store.Update(func(tx store.Tx) error { return store.CreateSecret(tx, secret) }) assert.NoError(t, err) // updating a secret without providing an ID results in an InvalidArgument _, err = ts.Client.UpdateSecret(context.Background(), &api.UpdateSecretRequest{}) assert.Error(t, err) assert.Equal(t, codes.InvalidArgument, grpc.Code(err), grpc.ErrorDesc(err)) // getting a non-existent secret fails with NotFound _, err = ts.Client.UpdateSecret(context.Background(), &api.UpdateSecretRequest{SecretID: "1234adsaa", SecretVersion: &api.Version{Index: 1}}) assert.Error(t, err) assert.Equal(t, codes.NotFound, grpc.Code(err), grpc.ErrorDesc(err)) // updating an existing secret's data returns an error secret.Spec.Data = []byte{1} resp, err := ts.Client.UpdateSecret(context.Background(), &api.UpdateSecretRequest{ SecretID: secret.ID, Spec: &secret.Spec, SecretVersion: &secret.Meta.Version, }) assert.Equal(t, codes.InvalidArgument, grpc.Code(err), grpc.ErrorDesc(err)) // updating an existing secret's Name returns an error secret.Spec.Data = nil secret.Spec.Annotations.Name = "AnotherName" resp, err = ts.Client.UpdateSecret(context.Background(), &api.UpdateSecretRequest{ SecretID: secret.ID, Spec: &secret.Spec, SecretVersion: &secret.Meta.Version, }) assert.Equal(t, codes.InvalidArgument, grpc.Code(err), grpc.ErrorDesc(err)) // updating the secret with the original spec succeeds secret.Spec.Data = []byte("data") secret.Spec.Annotations.Name = "name" assert.NotNil(t, secret.Spec.Data) resp, err = ts.Client.UpdateSecret(context.Background(), &api.UpdateSecretRequest{ SecretID: secret.ID, Spec: &secret.Spec, SecretVersion: &secret.Meta.Version, }) assert.NoError(t, err) assert.NotNil(t, resp) assert.NotNil(t, resp.Secret) // updating an existing secret's labels returns the secret with all the private data cleaned newLabels := map[string]string{"mod2": "0", "mod4": "0", "mod6": "0"} secret.Spec.Annotations.Labels = newLabels secret.Spec.Data = nil resp, err = ts.Client.UpdateSecret(context.Background(), &api.UpdateSecretRequest{ SecretID: secret.ID, Spec: &secret.Spec, SecretVersion: &resp.Secret.Meta.Version, }) assert.NoError(t, err) assert.NotNil(t, resp) assert.NotNil(t, resp.Secret) assert.Nil(t, resp.Secret.Spec.Data) assert.Equal(t, resp.Secret.Spec.Annotations.Labels, newLabels) // updating a secret with nil data and correct name succeeds again secret.Spec.Data = nil secret.Spec.Annotations.Name = "name" resp, err = ts.Client.UpdateSecret(context.Background(), &api.UpdateSecretRequest{ SecretID: secret.ID, Spec: &secret.Spec, SecretVersion: &resp.Secret.Meta.Version, }) assert.NoError(t, err) assert.NotNil(t, resp) assert.NotNil(t, resp.Secret) assert.Nil(t, resp.Secret.Spec.Data) assert.Equal(t, resp.Secret.Spec.Annotations.Labels, newLabels) }
// If a secret is updated or deleted, even if it's for an existing task, no changes will be sent down func TestAssignmentsSecretUpdateAndDeletion(t *testing.T) { t.Parallel() gd, err := startDispatcher(DefaultConfig()) assert.NoError(t, err) defer gd.Close() expectedSessionID, nodeID := getSessionAndNodeID(t, gd.Clients[0]) // create the relevant secrets and tasks secrets, tasks := makeTasksAndSecrets(t, nodeID) err = gd.Store.Update(func(tx store.Tx) error { for _, secret := range secrets[:len(secrets)-1] { assert.NoError(t, store.CreateSecret(tx, secret)) } for _, task := range tasks { assert.NoError(t, store.CreateTask(tx, task)) } return nil }) assert.NoError(t, err) stream, err := gd.Clients[0].Assignments(context.Background(), &api.AssignmentsRequest{SessionID: expectedSessionID}) assert.NoError(t, err) defer stream.CloseSend() time.Sleep(100 * time.Millisecond) // check the initial task and secret stream resp, err := stream.Recv() assert.NoError(t, err) // FIXME(aaronl): This is hard to maintain. assert.Equal(t, 16, len(resp.Changes)) taskChanges, secretChanges := collectTasksAndSecrets(resp.Changes) assert.Len(t, taskChanges, 10) // 10 types of task states >= assigned, 2 types < assigned for _, task := range tasks[2:] { assert.NotNil(t, taskChanges[idAndAction{id: task.ID, action: api.AssignmentChange_AssignmentActionUpdate}]) } assert.Len(t, secretChanges, 6) // 6 types of task states between assigned and running inclusive for _, secret := range secrets[2:8] { assert.NotNil(t, secretChanges[idAndAction{id: secret.ID, action: api.AssignmentChange_AssignmentActionUpdate}]) } // updating secrets, used by tasks or not, do not cause any changes err = gd.Store.Update(func(tx store.Tx) error { for _, secret := range secrets[:len(secrets)-2] { secret.Spec.Data = []byte("new secret data") assert.NoError(t, store.UpdateSecret(tx, secret)) } return nil }) assert.NoError(t, err) recvChan := make(chan struct{}) go func() { _, _ = stream.Recv() recvChan <- struct{}{} }() select { case <-recvChan: assert.Fail(t, "secret update should not trigger dispatcher update") case <-time.After(250 * time.Millisecond): } // deleting secrets, used by tasks or not, do not cause any changes err = gd.Store.Update(func(tx store.Tx) error { for _, secret := range secrets[:len(secrets)-2] { assert.NoError(t, store.DeleteSecret(tx, secret.ID)) } return nil }) assert.NoError(t, err) select { case <-recvChan: assert.Fail(t, "secret delete should not trigger dispatcher update") case <-time.After(250 * time.Millisecond): } }
// As tasks are added, assignments will send down tasks > ASSIGNED, and any secrets // for said tasks that are <= RUNNING (if the secrets exist) func TestAssignmentsAddingTasks(t *testing.T) { t.Parallel() gd, err := startDispatcher(DefaultConfig()) assert.NoError(t, err) defer gd.Close() expectedSessionID, nodeID := getSessionAndNodeID(t, gd.Clients[0]) stream, err := gd.Clients[0].Assignments(context.Background(), &api.AssignmentsRequest{SessionID: expectedSessionID}) assert.NoError(t, err) defer stream.CloseSend() time.Sleep(100 * time.Millisecond) // There are no initial tasks or secrets resp, err := stream.Recv() assert.NoError(t, err) assert.Empty(t, resp.Changes) // create the relevant secrets and tasks and update the tasks secrets, tasks := makeTasksAndSecrets(t, nodeID) err = gd.Store.Update(func(tx store.Tx) error { for _, secret := range secrets[:len(secrets)-1] { assert.NoError(t, store.CreateSecret(tx, secret)) } for _, task := range tasks { assert.NoError(t, store.CreateTask(tx, task)) } return nil }) assert.NoError(t, err) // Nothing happens until we update. Updating all the tasks will send updates for all the tasks >= ASSIGNED (10), // and secrets for all the tasks >= ASSIGNED and <= RUNNING (6). err = gd.Store.Update(func(tx store.Tx) error { for _, task := range tasks { assert.NoError(t, store.UpdateTask(tx, task)) } return nil }) assert.NoError(t, err) resp, err = stream.Recv() assert.NoError(t, err) // FIXME(aaronl): This is hard to maintain. assert.Equal(t, 10+6, len(resp.Changes)) taskChanges, secretChanges := collectTasksAndSecrets(resp.Changes) assert.Len(t, taskChanges, 10) for _, task := range tasks[2:] { assert.NotNil(t, taskChanges[idAndAction{id: task.ID, action: api.AssignmentChange_AssignmentActionUpdate}]) } assert.Len(t, secretChanges, 6) // all the secrets for tasks >= ASSIGNED and <= RUNNING for _, secret := range secrets[2:8] { assert.NotNil(t, secretChanges[idAndAction{id: secret.ID, action: api.AssignmentChange_AssignmentActionUpdate}]) } // deleting the tasks removes all the secrets for every single task, no matter // what state it's in err = gd.Store.Update(func(tx store.Tx) error { for _, task := range tasks { assert.NoError(t, store.DeleteTask(tx, task.ID)) } return nil }) assert.NoError(t, err) // updates for all the tasks >= ASSIGNMENT, and remove secrets for all of them, even ones that don't exist // (there will be 2 tasks changes that won't be sent down) resp, err = stream.Recv() assert.NoError(t, err) assert.Equal(t, len(tasks)-2+len(secrets)-2, len(resp.Changes)) taskChanges, secretChanges = collectTasksAndSecrets(resp.Changes) assert.Len(t, taskChanges, len(tasks)-2) for _, task := range tasks[2:] { assert.NotNil(t, taskChanges[idAndAction{id: task.ID, action: api.AssignmentChange_AssignmentActionRemove}]) } assert.Len(t, secretChanges, len(secrets)-2) for _, secret := range secrets[2:] { assert.NotNil(t, secretChanges[idAndAction{id: secret.ID, action: api.AssignmentChange_AssignmentActionRemove}]) } }
// Assignments will send down any existing node tasks > ASSIGNED, and any secrets // for said tasks that are <= RUNNING (if the secrets exist) func TestAssignmentsInitialNodeTasks(t *testing.T) { t.Parallel() gd, err := startDispatcher(DefaultConfig()) assert.NoError(t, err) defer gd.Close() expectedSessionID, nodeID := getSessionAndNodeID(t, gd.Clients[0]) // create the relevant secrets and tasks secrets, tasks := makeTasksAndSecrets(t, nodeID) err = gd.Store.Update(func(tx store.Tx) error { for _, secret := range secrets[:] { assert.NoError(t, store.CreateSecret(tx, secret)) } for _, task := range tasks { assert.NoError(t, store.CreateTask(tx, task)) } return nil }) assert.NoError(t, err) stream, err := gd.Clients[0].Assignments(context.Background(), &api.AssignmentsRequest{SessionID: expectedSessionID}) assert.NoError(t, err) defer stream.CloseSend() time.Sleep(100 * time.Millisecond) // check the initial task and secret stream resp, err := stream.Recv() assert.NoError(t, err) // FIXME(aaronl): This is hard to maintain. assert.Equal(t, 17, len(resp.Changes)) taskChanges, secretChanges := collectTasksAndSecrets(resp.Changes) assert.Len(t, taskChanges, 10) // 10 types of task states >= assigned, 2 types < assigned for _, task := range tasks[2:] { assert.NotNil(t, taskChanges[idAndAction{id: task.ID, action: api.AssignmentChange_AssignmentActionUpdate}]) } assert.Len(t, secretChanges, 7) // 6 different secrets for states between assigned and running inclusive plus secret12 for _, secret := range secrets[2:8] { assert.NotNil(t, secretChanges[idAndAction{id: secret.ID, action: api.AssignmentChange_AssignmentActionUpdate}]) } // updating all the tasks will attempt to remove all the secrets for the tasks that are in state > running err = gd.Store.Update(func(tx store.Tx) error { for _, task := range tasks { assert.NoError(t, store.UpdateTask(tx, task)) } return nil }) assert.NoError(t, err) // updates for all the tasks, remove secret sent for the 4 types of states > running resp, err = stream.Recv() assert.NoError(t, err) assert.Equal(t, 5, len(resp.Changes)) taskChanges, secretChanges = collectTasksAndSecrets(resp.Changes) assert.Len(t, taskChanges, 1) assert.NotNil(t, taskChanges[idAndAction{id: tasks[2].ID, action: api.AssignmentChange_AssignmentActionUpdate}]) // this is the task in ASSIGNED assert.Len(t, secretChanges, 4) // these are the secrets for states > running for _, secret := range secrets[9 : len(secrets)-1] { assert.NotNil(t, secretChanges[idAndAction{id: secret.ID, action: api.AssignmentChange_AssignmentActionRemove}]) } // deleting the tasks removes all the secrets for every single task, no matter // what state it's in err = gd.Store.Update(func(tx store.Tx) error { for _, task := range tasks { assert.NoError(t, store.DeleteTask(tx, task.ID)) } return nil }) assert.NoError(t, err) // updates for all the tasks >= ASSIGNMENT, and remove secrets for all of them, // (there will be 2 tasks changes that won't be sent down) resp, err = stream.Recv() assert.NoError(t, err) assert.Equal(t, len(tasks)-2+len(secrets)-2, len(resp.Changes)) taskChanges, secretChanges = collectTasksAndSecrets(resp.Changes) assert.Len(t, taskChanges, len(tasks)-2) for _, task := range tasks[2:] { assert.NotNil(t, taskChanges[idAndAction{id: task.ID, action: api.AssignmentChange_AssignmentActionRemove}]) } assert.Len(t, secretChanges, len(secrets)-2) for _, secret := range secrets[2:] { assert.NotNil(t, secretChanges[idAndAction{id: secret.ID, action: api.AssignmentChange_AssignmentActionRemove}]) } }