docker/swarmkit

View on GitHub
manager/controlapi/secret.go

Summary

Maintainability
C
1 day
Test Coverage
package controlapi

import (
    "context"
    "crypto/subtle"
    "strings"

    "github.com/moby/swarmkit/v2/api"
    "github.com/moby/swarmkit/v2/api/validation"
    "github.com/moby/swarmkit/v2/identity"
    "github.com/moby/swarmkit/v2/log"
    "github.com/moby/swarmkit/v2/manager/state/store"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
)

// assumes spec is not nil
func secretFromSecretSpec(spec *api.SecretSpec) *api.Secret {
    return &api.Secret{
        ID:   identity.NewID(),
        Spec: *spec,
    }
}

// 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, status.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, status.Errorf(codes.NotFound, "secret %s not found", request.SecretID)
    }

    secret.Spec.Data = nil // clean the actual secret data so it's never returned
    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, status.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 status.Errorf(codes.NotFound, "secret %s not found", request.SecretID)
        }

        // 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 status.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
    }

    log.G(ctx).WithFields(log.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
}

// ListSecrets returns a `ListSecretResponse` with a list all non-internal `Secret`s being
// managed, or all secrets matching any name in `ListSecretsRequest.Names`, any
// name prefix in `ListSecretsRequest.NamePrefixes`, any id in
// `ListSecretsRequest.SecretIDs`, or any id prefix in `ListSecretsRequest.IDPrefixes`.
// - Returns an error if listing fails.
func (s *Server) ListSecrets(ctx context.Context, request *api.ListSecretsRequest) (*api.ListSecretsResponse, error) {
    var (
        secrets     []*api.Secret
        respSecrets []*api.Secret
        err         error
        byFilters   []store.By
        by          store.By
        labels      map[string]string
    )

    // return all secrets that match either any of the names or any of the name prefixes (why would you give both?)
    if request.Filters != nil {
        for _, name := range request.Filters.Names {
            byFilters = append(byFilters, store.ByName(name))
        }
        for _, prefix := range request.Filters.NamePrefixes {
            byFilters = append(byFilters, store.ByNamePrefix(prefix))
        }
        for _, prefix := range request.Filters.IDPrefixes {
            byFilters = append(byFilters, store.ByIDPrefix(prefix))
        }
        labels = request.Filters.Labels
    }

    switch len(byFilters) {
    case 0:
        by = store.All
    case 1:
        by = byFilters[0]
    default:
        by = store.Or(byFilters...)
    }

    s.store.View(func(tx store.ReadTx) {
        secrets, err = store.FindSecrets(tx, by)
    })
    if err != nil {
        return nil, err
    }

    // strip secret data from the secret, filter by label, and filter out all internal secrets
    for _, secret := range secrets {
        if secret.Internal || !filterMatchLabels(secret.Spec.Annotations.Labels, labels) {
            continue
        }
        secret.Spec.Data = nil // clean the actual secret data so it's never returned
        respSecrets = append(respSecrets, secret)
    }

    return &api.ListSecretsResponse{Secrets: respSecrets}, nil
}

// CreateSecret creates and returns 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
    }

    if request.Spec.Driver != nil { // Check that the requested driver is valid
        if _, err := s.dr.NewSecretDriver(request.Spec.Driver); 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, status.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(log.Fields{
            "secret.Name": request.Spec.Annotations.Name,
            "method":      "CreateSecret",
        }).Debugf("secret created")

        return &api.CreateSecretResponse{Secret: secret}, nil
    default:
        return nil, err
    }
}

// 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, status.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 status.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 status.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 status.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, status.Errorf(codes.NotFound, "secret %s not found", request.SecretID)
    case nil:
        log.G(ctx).WithFields(log.Fields{
            "secret.ID": request.SecretID,
            "method":    "RemoveSecret",
        }).Debugf("secret removed")

        return &api.RemoveSecretResponse{}, nil
    default:
        return nil, err
    }
}

func validateSecretSpec(spec *api.SecretSpec) error {
    if spec == nil {
        return status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
    }
    if err := validateConfigOrSecretAnnotations(spec.Annotations); err != nil {
        return err
    }
    // Check if secret driver is defined
    if spec.Driver != nil {
        // Ensure secret driver has a name
        if spec.Driver.Name == "" {
            return status.Errorf(codes.InvalidArgument, "secret driver must have a name")
        }
        return nil
    }
    if err := validation.ValidateSecretPayload(spec.Data); err != nil {
        return status.Errorf(codes.InvalidArgument, "%s", err.Error())
    }
    return nil
}