2016-06-07 17:28:28 -04:00
|
|
|
package controlapi
|
|
|
|
|
|
|
|
import (
|
|
|
|
"strings"
|
2016-10-26 09:35:48 -04:00
|
|
|
"time"
|
2016-06-07 17:28:28 -04:00
|
|
|
|
|
|
|
"github.com/docker/swarmkit/api"
|
|
|
|
"github.com/docker/swarmkit/ca"
|
2017-03-28 14:51:33 -04:00
|
|
|
"github.com/docker/swarmkit/log"
|
2016-10-21 15:53:24 -04:00
|
|
|
"github.com/docker/swarmkit/manager/encryption"
|
2016-06-07 17:28:28 -04:00
|
|
|
"github.com/docker/swarmkit/manager/state/store"
|
2017-01-23 18:50:10 -05:00
|
|
|
gogotypes "github.com/gogo/protobuf/types"
|
2016-06-07 17:28:28 -04:00
|
|
|
"golang.org/x/net/context"
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
)
|
|
|
|
|
2016-10-26 09:35:48 -04:00
|
|
|
const (
|
|
|
|
// expiredCertGrace is the amount of time to keep a node in the
|
|
|
|
// blacklist beyond its certificate expiration timestamp.
|
|
|
|
expiredCertGrace = 24 * time.Hour * 7
|
|
|
|
)
|
|
|
|
|
2016-06-07 17:28:28 -04:00
|
|
|
func validateClusterSpec(spec *api.ClusterSpec) error {
|
|
|
|
if spec == nil {
|
|
|
|
return grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
|
|
|
|
}
|
|
|
|
|
2016-06-17 22:01:18 -04:00
|
|
|
// Validate that expiry time being provided is valid, and over our minimum
|
2016-06-07 17:28:28 -04:00
|
|
|
if spec.CAConfig.NodeCertExpiry != nil {
|
2017-01-23 18:50:10 -05:00
|
|
|
expiry, err := gogotypes.DurationFromProto(spec.CAConfig.NodeCertExpiry)
|
2016-06-07 17:28:28 -04:00
|
|
|
if err != nil {
|
|
|
|
return grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
|
|
|
|
}
|
|
|
|
if expiry < ca.MinNodeCertExpiration {
|
|
|
|
return grpc.Errorf(codes.InvalidArgument, "minimum certificate expiry time is: %s", ca.MinNodeCertExpiration)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate that AcceptancePolicies only include Secrets that are bcrypted
|
2017-04-03 23:54:30 -04:00
|
|
|
// TODO(diogo): Add a global list of acceptance algorithms. We only support bcrypt for now.
|
2016-06-07 17:28:28 -04:00
|
|
|
if len(spec.AcceptancePolicy.Policies) > 0 {
|
|
|
|
for _, policy := range spec.AcceptancePolicy.Policies {
|
|
|
|
if policy.Secret != nil && strings.ToLower(policy.Secret.Alg) != "bcrypt" {
|
|
|
|
return grpc.Errorf(codes.InvalidArgument, "hashing algorithm is not supported: %s", policy.Secret.Alg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-17 22:01:18 -04:00
|
|
|
// Validate that heartbeatPeriod time being provided is valid
|
|
|
|
if spec.Dispatcher.HeartbeatPeriod != nil {
|
2017-01-23 18:50:10 -05:00
|
|
|
heartbeatPeriod, err := gogotypes.DurationFromProto(spec.Dispatcher.HeartbeatPeriod)
|
2016-06-17 22:01:18 -04:00
|
|
|
if err != nil {
|
|
|
|
return grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
|
|
|
|
}
|
|
|
|
if heartbeatPeriod < 0 {
|
|
|
|
return grpc.Errorf(codes.InvalidArgument, "heartbeat time period cannot be a negative duration")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-07 17:28:28 -04:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCluster returns a Cluster given a ClusterID.
|
|
|
|
// - Returns `InvalidArgument` if ClusterID is not provided.
|
|
|
|
// - Returns `NotFound` if the Cluster is not found.
|
|
|
|
func (s *Server) GetCluster(ctx context.Context, request *api.GetClusterRequest) (*api.GetClusterResponse, error) {
|
|
|
|
if request.ClusterID == "" {
|
|
|
|
return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
var cluster *api.Cluster
|
|
|
|
s.store.View(func(tx store.ReadTx) {
|
|
|
|
cluster = store.GetCluster(tx, request.ClusterID)
|
|
|
|
})
|
|
|
|
if cluster == nil {
|
|
|
|
return nil, grpc.Errorf(codes.NotFound, "cluster %s not found", request.ClusterID)
|
|
|
|
}
|
|
|
|
|
|
|
|
redactedClusters := redactClusters([]*api.Cluster{cluster})
|
|
|
|
|
|
|
|
// WARN: we should never return cluster here. We need to redact the private fields first.
|
|
|
|
return &api.GetClusterResponse{
|
|
|
|
Cluster: redactedClusters[0],
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateCluster updates a Cluster referenced by ClusterID with the given ClusterSpec.
|
|
|
|
// - Returns `NotFound` if the Cluster is not found.
|
|
|
|
// - Returns `InvalidArgument` if the ClusterSpec is malformed.
|
|
|
|
// - Returns `Unimplemented` if the ClusterSpec references unimplemented features.
|
|
|
|
// - Returns an error if the update fails.
|
|
|
|
func (s *Server) UpdateCluster(ctx context.Context, request *api.UpdateClusterRequest) (*api.UpdateClusterResponse, error) {
|
|
|
|
if request.ClusterID == "" || request.ClusterVersion == nil {
|
|
|
|
return nil, grpc.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
|
|
|
|
}
|
|
|
|
if err := validateClusterSpec(request.Spec); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var cluster *api.Cluster
|
|
|
|
err := s.store.Update(func(tx store.Tx) error {
|
|
|
|
cluster = store.GetCluster(tx, request.ClusterID)
|
|
|
|
if cluster == nil {
|
2016-12-04 23:56:40 -05:00
|
|
|
return grpc.Errorf(codes.NotFound, "cluster %s not found", request.ClusterID)
|
2016-06-07 17:28:28 -04:00
|
|
|
}
|
2017-03-28 14:51:33 -04:00
|
|
|
// This ensures that we always have the latest security config, so our ca.SecurityConfig.RootCA and
|
|
|
|
// ca.SecurityConfig.externalCA objects are up-to-date with the current api.Cluster.RootCA and
|
|
|
|
// api.Cluster.Spec.ExternalCA objects, respectively. Note that if, during this update, the cluster gets
|
|
|
|
// updated again with different CA info and the security config gets changed under us, that's still fine because
|
|
|
|
// this cluster update would fail anyway due to its version being too low on write.
|
|
|
|
if err := s.scu.UpdateRootCA(ctx, cluster); err != nil {
|
|
|
|
log.G(ctx).WithField(
|
|
|
|
"method", "(*controlapi.Server).UpdateCluster").WithError(err).Error("could not update security config")
|
|
|
|
return grpc.Errorf(codes.Internal, "could not update security config")
|
|
|
|
}
|
|
|
|
rootCA := s.securityConfig.RootCA()
|
|
|
|
|
2016-06-07 17:28:28 -04:00
|
|
|
cluster.Meta.Version = *request.ClusterVersion
|
|
|
|
cluster.Spec = *request.Spec.Copy()
|
2016-07-20 20:34:33 -04:00
|
|
|
|
2016-10-26 09:35:48 -04:00
|
|
|
expireBlacklistedCerts(cluster)
|
|
|
|
|
2016-10-21 15:53:24 -04:00
|
|
|
if request.Rotation.WorkerJoinToken {
|
2017-03-28 14:51:33 -04:00
|
|
|
cluster.RootCA.JoinTokens.Worker = ca.GenerateJoinToken(rootCA)
|
2016-07-20 20:34:33 -04:00
|
|
|
}
|
2016-10-21 15:53:24 -04:00
|
|
|
if request.Rotation.ManagerJoinToken {
|
2017-03-28 14:51:33 -04:00
|
|
|
cluster.RootCA.JoinTokens.Manager = ca.GenerateJoinToken(rootCA)
|
2016-07-20 20:34:33 -04:00
|
|
|
}
|
2016-10-21 15:53:24 -04:00
|
|
|
|
2017-03-30 19:12:33 -04:00
|
|
|
updatedRootCA, err := validateCAConfig(ctx, s.securityConfig, cluster)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
cluster.RootCA = *updatedRootCA
|
|
|
|
|
2016-10-21 15:53:24 -04:00
|
|
|
var unlockKeys []*api.EncryptionKey
|
|
|
|
var managerKey *api.EncryptionKey
|
|
|
|
for _, eKey := range cluster.UnlockKeys {
|
|
|
|
if eKey.Subsystem == ca.ManagerRole {
|
|
|
|
if !cluster.Spec.EncryptionConfig.AutoLockManagers {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
managerKey = eKey
|
|
|
|
}
|
|
|
|
unlockKeys = append(unlockKeys, eKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case !cluster.Spec.EncryptionConfig.AutoLockManagers:
|
|
|
|
break
|
|
|
|
case managerKey == nil:
|
|
|
|
unlockKeys = append(unlockKeys, &api.EncryptionKey{
|
|
|
|
Subsystem: ca.ManagerRole,
|
|
|
|
Key: encryption.GenerateSecretKey(),
|
|
|
|
})
|
|
|
|
case request.Rotation.ManagerUnlockKey:
|
|
|
|
managerKey.Key = encryption.GenerateSecretKey()
|
|
|
|
}
|
|
|
|
cluster.UnlockKeys = unlockKeys
|
|
|
|
|
2016-06-07 17:28:28 -04:00
|
|
|
return store.UpdateCluster(tx, cluster)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
redactedClusters := redactClusters([]*api.Cluster{cluster})
|
|
|
|
|
|
|
|
// WARN: we should never return cluster here. We need to redact the private fields first.
|
|
|
|
return &api.UpdateClusterResponse{
|
|
|
|
Cluster: redactedClusters[0],
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func filterClusters(candidates []*api.Cluster, filters ...func(*api.Cluster) bool) []*api.Cluster {
|
|
|
|
result := []*api.Cluster{}
|
|
|
|
|
|
|
|
for _, c := range candidates {
|
|
|
|
match := true
|
|
|
|
for _, f := range filters {
|
|
|
|
if !f(c) {
|
|
|
|
match = false
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if match {
|
|
|
|
result = append(result, c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListClusters returns a list of all clusters.
|
|
|
|
func (s *Server) ListClusters(ctx context.Context, request *api.ListClustersRequest) (*api.ListClustersResponse, error) {
|
|
|
|
var (
|
|
|
|
clusters []*api.Cluster
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
s.store.View(func(tx store.ReadTx) {
|
|
|
|
switch {
|
|
|
|
case request.Filters != nil && len(request.Filters.Names) > 0:
|
|
|
|
clusters, err = store.FindClusters(tx, buildFilters(store.ByName, request.Filters.Names))
|
2016-07-20 11:16:54 -04:00
|
|
|
case request.Filters != nil && len(request.Filters.NamePrefixes) > 0:
|
|
|
|
clusters, err = store.FindClusters(tx, buildFilters(store.ByNamePrefix, request.Filters.NamePrefixes))
|
2016-06-07 17:28:28 -04:00
|
|
|
case request.Filters != nil && len(request.Filters.IDPrefixes) > 0:
|
|
|
|
clusters, err = store.FindClusters(tx, buildFilters(store.ByIDPrefix, request.Filters.IDPrefixes))
|
|
|
|
default:
|
|
|
|
clusters, err = store.FindClusters(tx, store.All)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if request.Filters != nil {
|
|
|
|
clusters = filterClusters(clusters,
|
|
|
|
func(e *api.Cluster) bool {
|
|
|
|
return filterContains(e.Spec.Annotations.Name, request.Filters.Names)
|
|
|
|
},
|
2016-07-20 11:16:54 -04:00
|
|
|
func(e *api.Cluster) bool {
|
|
|
|
return filterContainsPrefix(e.Spec.Annotations.Name, request.Filters.NamePrefixes)
|
|
|
|
},
|
2016-06-07 17:28:28 -04:00
|
|
|
func(e *api.Cluster) bool {
|
|
|
|
return filterContainsPrefix(e.ID, request.Filters.IDPrefixes)
|
|
|
|
},
|
|
|
|
func(e *api.Cluster) bool {
|
|
|
|
return filterMatchLabels(e.Spec.Annotations.Labels, request.Filters.Labels)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// WARN: we should never return cluster here. We need to redact the private fields first.
|
|
|
|
return &api.ListClustersResponse{
|
|
|
|
Clusters: redactClusters(clusters),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// redactClusters is a method that enforces a whitelist of fields that are ok to be
|
2016-08-09 14:49:39 -04:00
|
|
|
// returned in the Cluster object. It should filter out all sensitive information.
|
2016-06-07 17:28:28 -04:00
|
|
|
func redactClusters(clusters []*api.Cluster) []*api.Cluster {
|
|
|
|
var redactedClusters []*api.Cluster
|
|
|
|
// Only add public fields to the new clusters
|
|
|
|
for _, cluster := range clusters {
|
|
|
|
// Copy all the mandatory fields
|
2017-03-30 19:12:33 -04:00
|
|
|
// Do not copy secret keys
|
|
|
|
redactedSpec := cluster.Spec.Copy()
|
|
|
|
redactedSpec.CAConfig.SigningCAKey = nil
|
|
|
|
|
|
|
|
redactedRootCA := cluster.RootCA.Copy()
|
|
|
|
redactedRootCA.CAKey = nil
|
|
|
|
if r := redactedRootCA.RootRotation; r != nil {
|
|
|
|
r.CAKey = nil
|
|
|
|
}
|
2016-06-07 17:28:28 -04:00
|
|
|
newCluster := &api.Cluster{
|
2017-03-30 19:12:33 -04:00
|
|
|
ID: cluster.ID,
|
|
|
|
Meta: cluster.Meta,
|
|
|
|
Spec: *redactedSpec,
|
|
|
|
RootCA: *redactedRootCA,
|
2016-10-26 09:35:48 -04:00
|
|
|
BlacklistedCertificates: cluster.BlacklistedCertificates,
|
2016-06-07 17:28:28 -04:00
|
|
|
}
|
|
|
|
redactedClusters = append(redactedClusters, newCluster)
|
|
|
|
}
|
|
|
|
|
|
|
|
return redactedClusters
|
|
|
|
}
|
2016-10-26 09:35:48 -04:00
|
|
|
|
|
|
|
func expireBlacklistedCerts(cluster *api.Cluster) {
|
|
|
|
nowMinusGrace := time.Now().Add(-expiredCertGrace)
|
|
|
|
|
|
|
|
for cn, blacklistedCert := range cluster.BlacklistedCertificates {
|
|
|
|
if blacklistedCert.Expiry == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2017-01-23 18:50:10 -05:00
|
|
|
expiry, err := gogotypes.TimestampFromProto(blacklistedCert.Expiry)
|
2016-10-26 09:35:48 -04:00
|
|
|
if err == nil && nowMinusGrace.After(expiry) {
|
|
|
|
delete(cluster.BlacklistedCertificates, cn)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|