// checkSecretExistence finds if the secret exists func (s *Server) checkSecretExistence(tx store.Tx, spec *api.ServiceSpec) error { container := spec.Task.GetContainer() if container == nil { return nil } var failedSecrets []string for _, secretRef := range container.Secrets { secret := store.GetSecret(tx, secretRef.SecretID) // Check to see if the secret exists and secretRef.SecretName matches the actual secretName if secret == nil || secret.Spec.Annotations.Name != secretRef.SecretName { failedSecrets = append(failedSecrets, secretRef.SecretName) } } if len(failedSecrets) > 0 { secretStr := "secrets" if len(failedSecrets) == 1 { secretStr = "secret" } return grpc.Errorf(codes.InvalidArgument, "%s not found: %v", secretStr, strings.Join(failedSecrets, ", ")) } return nil }
// UpdateSecret updates a Secret referenced by SecretID with the given SecretSpec. // - Returns `NotFound` if the Secret is not found. // - Returns `InvalidArgument` if the SecretSpec is malformed or anything other than Labels is changed // - Returns an error if the update fails. func (s *Server) UpdateSecret(ctx context.Context, request *api.UpdateSecretRequest) (*api.UpdateSecretResponse, error) { if request.SecretID == "" || request.SecretVersion == nil { return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error()) } var secret *api.Secret err := s.store.Update(func(tx store.Tx) error { secret = store.GetSecret(tx, request.SecretID) if secret == nil { return nil } if secret.Spec.Annotations.Name != request.Spec.Annotations.Name || request.Spec.Data != nil { return grpc.Errorf(codes.InvalidArgument, "only updates to Labels are allowed") } // We only allow updating Labels secret.Meta.Version = *request.SecretVersion secret.Spec.Annotations.Labels = request.Spec.Annotations.Labels return store.UpdateSecret(tx, secret) }) if err != nil { return nil, err } if secret == nil { return nil, grpc.Errorf(codes.NotFound, "secret %s not found", request.SecretID) } // WARN: we should never return the actual secret data here. We need to redact the private fields first. secret.Spec.Data = nil return &api.UpdateSecretResponse{ Secret: secret, }, nil }
// RemoveSecret removes the secret referenced by `RemoveSecretRequest.ID`. // - Returns `InvalidArgument` if `RemoveSecretRequest.ID` is empty. // - Returns `NotFound` if the a secret named `RemoveSecretRequest.ID` is not found. // - Returns `SecretInUse` if the secret is currently in use // - Returns an error if the deletion fails. func (s *Server) RemoveSecret(ctx context.Context, request *api.RemoveSecretRequest) (*api.RemoveSecretResponse, error) { if request.SecretID == "" { return nil, grpc.Errorf(codes.InvalidArgument, "secret ID must be provided") } err := s.store.Update(func(tx store.Tx) error { // Check if the secret exists secret := store.GetSecret(tx, request.SecretID) if secret == nil { return grpc.Errorf(codes.NotFound, "could not find secret %s", request.SecretID) } // Check if any services currently reference this secret, return error if so services, err := store.FindServices(tx, store.ByReferencedSecretID(request.SecretID)) if err != nil { return grpc.Errorf(codes.Internal, "could not find services using secret %s: %v", request.SecretID, err) } if len(services) != 0 { serviceNames := make([]string, 0, len(services)) for _, service := range services { serviceNames = append(serviceNames, service.Spec.Annotations.Name) } secretName := secret.Spec.Annotations.Name serviceNameStr := strings.Join(serviceNames, ", ") serviceStr := "services" if len(serviceNames) == 1 { serviceStr = "service" } return grpc.Errorf(codes.InvalidArgument, "secret '%s' is in use by the following %s: %v", secretName, serviceStr, serviceNameStr) } return store.DeleteSecret(tx, request.SecretID) }) switch err { case store.ErrNotExist: return nil, grpc.Errorf(codes.NotFound, "secret %s not found", request.SecretID) case nil: log.G(ctx).WithFields(logrus.Fields{ "secret.ID": request.SecretID, "method": "RemoveSecret", }).Debugf("secret removed") return &api.RemoveSecretResponse{}, nil default: return nil, err } }
// GetSecret returns a `GetSecretResponse` with a `Secret` with the same // id as `GetSecretRequest.SecretID` // - Returns `NotFound` if the Secret with the given id is not found. // - Returns `InvalidArgument` if the `GetSecretRequest.SecretID` is empty. // - Returns an error if getting fails. func (s *Server) GetSecret(ctx context.Context, request *api.GetSecretRequest) (*api.GetSecretResponse, error) { if request.SecretID == "" { return nil, grpc.Errorf(codes.InvalidArgument, "secret ID must be provided") } var secret *api.Secret s.store.View(func(tx store.ReadTx) { secret = store.GetSecret(tx, request.SecretID) }) if secret == nil { return nil, grpc.Errorf(codes.NotFound, "secret %s not found", request.SecretID) } return &api.GetSecretResponse{Secret: secret}, nil }
// UpdateSecret updates a Secret referenced by SecretID with the given SecretSpec. // - Returns `NotFound` if the Secret is not found. // - Returns `InvalidArgument` if the SecretSpec is malformed or anything other than Labels is changed // - Returns an error if the update fails. func (s *Server) UpdateSecret(ctx context.Context, request *api.UpdateSecretRequest) (*api.UpdateSecretResponse, error) { if request.SecretID == "" || request.SecretVersion == nil { return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error()) } var secret *api.Secret err := s.store.Update(func(tx store.Tx) error { secret = store.GetSecret(tx, request.SecretID) if secret == nil { return nil } // Check if the Name is different than the current name, or the secret is non-nil and different // than the current secret if secret.Spec.Annotations.Name != request.Spec.Annotations.Name || (request.Spec.Data != nil && subtle.ConstantTimeCompare(request.Spec.Data, secret.Spec.Data) == 0) { return grpc.Errorf(codes.InvalidArgument, "only updates to Labels are allowed") } // We only allow updating Labels secret.Meta.Version = *request.SecretVersion secret.Spec.Annotations.Labels = request.Spec.Annotations.Labels return store.UpdateSecret(tx, secret) }) if err != nil { return nil, err } if secret == nil { return nil, grpc.Errorf(codes.NotFound, "secret %s not found", request.SecretID) } log.G(ctx).WithFields(logrus.Fields{ "secret.ID": request.SecretID, "secret.Name": request.Spec.Annotations.Name, "method": "UpdateSecret", }).Debugf("secret updated") // WARN: we should never return the actual secret data here. We need to redact the private fields first. secret.Spec.Data = nil return &api.UpdateSecretResponse{ Secret: secret, }, nil }
func TestCreateSecret(t *testing.T) { ts := newTestServer(t) defer ts.Stop() // ---- creating a secret with an invalid spec fails, thus checking that CreateSecret validates the spec ---- _, err := ts.Client.CreateSecret(context.Background(), &api.CreateSecretRequest{Spec: createSecretSpec("", nil, nil)}) assert.Error(t, err) assert.Equal(t, codes.InvalidArgument, grpc.Code(err), grpc.ErrorDesc(err)) // ---- creating a secret with a valid spec succeeds, and returns a secret that reflects the secret in the store // exactly, but without the private data ---- data := []byte("secret") creationSpec := createSecretSpec("name", data, nil) validSpecRequest := api.CreateSecretRequest{Spec: creationSpec} resp, err := ts.Client.CreateSecret(context.Background(), &validSpecRequest) assert.NoError(t, err) assert.NotNil(t, resp) assert.NotNil(t, resp.Secret) // the data should be empty/omitted assert.Equal(t, *createSecretSpec("name", nil, nil), resp.Secret.Spec) // for sanity, check that the stored secret still has the secret data var storedSecret *api.Secret ts.Store.View(func(tx store.ReadTx) { storedSecret = store.GetSecret(tx, resp.Secret.ID) }) assert.NotNil(t, storedSecret) assert.Equal(t, data, storedSecret.Spec.Data) // ---- creating a secret with the same name, even if it's the exact same spec, fails due to a name conflict ---- _, err = ts.Client.CreateSecret(context.Background(), &validSpecRequest) assert.Error(t, err) assert.Equal(t, codes.AlreadyExists, grpc.Code(err), grpc.ErrorDesc(err)) }