github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/swarmkit/manager/controlapi/cluster.go (about)

     1  package controlapi
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/docker/swarmkit/api"
     9  	"github.com/docker/swarmkit/ca"
    10  	"github.com/docker/swarmkit/log"
    11  	"github.com/docker/swarmkit/manager/encryption"
    12  	"github.com/docker/swarmkit/manager/state/store"
    13  	gogotypes "github.com/gogo/protobuf/types"
    14  	"google.golang.org/grpc/codes"
    15  	"google.golang.org/grpc/status"
    16  )
    17  
    18  const (
    19  	// expiredCertGrace is the amount of time to keep a node in the
    20  	// blacklist beyond its certificate expiration timestamp.
    21  	expiredCertGrace = 24 * time.Hour * 7
    22  	// inbuilt default subnet size
    23  	inbuiltSubnetSize = 24
    24  	// VXLAN default port
    25  	defaultVXLANPort = 4789
    26  )
    27  
    28  var (
    29  	// inbuilt default address pool
    30  	inbuiltDefaultAddressPool = []string{"10.0.0.0/8"}
    31  )
    32  
    33  func validateClusterSpec(spec *api.ClusterSpec) error {
    34  	if spec == nil {
    35  		return status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
    36  	}
    37  
    38  	// Validate that expiry time being provided is valid, and over our minimum
    39  	if spec.CAConfig.NodeCertExpiry != nil {
    40  		expiry, err := gogotypes.DurationFromProto(spec.CAConfig.NodeCertExpiry)
    41  		if err != nil {
    42  			return status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
    43  		}
    44  		if expiry < ca.MinNodeCertExpiration {
    45  			return status.Errorf(codes.InvalidArgument, "minimum certificate expiry time is: %s", ca.MinNodeCertExpiration)
    46  		}
    47  	}
    48  
    49  	// Validate that AcceptancePolicies only include Secrets that are bcrypted
    50  	// TODO(diogo): Add a global list of acceptance algorithms. We only support bcrypt for now.
    51  	if len(spec.AcceptancePolicy.Policies) > 0 {
    52  		for _, policy := range spec.AcceptancePolicy.Policies {
    53  			if policy.Secret != nil && strings.ToLower(policy.Secret.Alg) != "bcrypt" {
    54  				return status.Errorf(codes.InvalidArgument, "hashing algorithm is not supported: %s", policy.Secret.Alg)
    55  			}
    56  		}
    57  	}
    58  
    59  	// Validate that heartbeatPeriod time being provided is valid
    60  	if spec.Dispatcher.HeartbeatPeriod != nil {
    61  		heartbeatPeriod, err := gogotypes.DurationFromProto(spec.Dispatcher.HeartbeatPeriod)
    62  		if err != nil {
    63  			return status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
    64  		}
    65  		if heartbeatPeriod < 0 {
    66  			return status.Errorf(codes.InvalidArgument, "heartbeat time period cannot be a negative duration")
    67  		}
    68  	}
    69  
    70  	if spec.Annotations.Name != store.DefaultClusterName {
    71  		return status.Errorf(codes.InvalidArgument, "modification of cluster name is not allowed")
    72  	}
    73  
    74  	return nil
    75  }
    76  
    77  // GetCluster returns a Cluster given a ClusterID.
    78  // - Returns `InvalidArgument` if ClusterID is not provided.
    79  // - Returns `NotFound` if the Cluster is not found.
    80  func (s *Server) GetCluster(ctx context.Context, request *api.GetClusterRequest) (*api.GetClusterResponse, error) {
    81  	if request.ClusterID == "" {
    82  		return nil, status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
    83  	}
    84  
    85  	var cluster *api.Cluster
    86  	s.store.View(func(tx store.ReadTx) {
    87  		cluster = store.GetCluster(tx, request.ClusterID)
    88  	})
    89  	if cluster == nil {
    90  		return nil, status.Errorf(codes.NotFound, "cluster %s not found", request.ClusterID)
    91  	}
    92  
    93  	redactedClusters := redactClusters([]*api.Cluster{cluster})
    94  
    95  	// WARN: we should never return cluster here. We need to redact the private fields first.
    96  	return &api.GetClusterResponse{
    97  		Cluster: redactedClusters[0],
    98  	}, nil
    99  }
   100  
   101  // UpdateCluster updates a Cluster referenced by ClusterID with the given ClusterSpec.
   102  // - Returns `NotFound` if the Cluster is not found.
   103  // - Returns `InvalidArgument` if the ClusterSpec is malformed.
   104  // - Returns `Unimplemented` if the ClusterSpec references unimplemented features.
   105  // - Returns an error if the update fails.
   106  func (s *Server) UpdateCluster(ctx context.Context, request *api.UpdateClusterRequest) (*api.UpdateClusterResponse, error) {
   107  	if request.ClusterID == "" || request.ClusterVersion == nil {
   108  		return nil, status.Errorf(codes.InvalidArgument, errInvalidArgument.Error())
   109  	}
   110  	if err := validateClusterSpec(request.Spec); err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	var cluster *api.Cluster
   115  	err := s.store.Update(func(tx store.Tx) error {
   116  		cluster = store.GetCluster(tx, request.ClusterID)
   117  		if cluster == nil {
   118  			return status.Errorf(codes.NotFound, "cluster %s not found", request.ClusterID)
   119  		}
   120  		// This ensures that we have the current rootCA with which to generate tokens (expiration doesn't matter
   121  		// for generating the tokens)
   122  		rootCA, err := ca.RootCAFromAPI(ctx, &cluster.RootCA, ca.DefaultNodeCertExpiration)
   123  		if err != nil {
   124  			log.G(ctx).WithField(
   125  				"method", "(*controlapi.Server).UpdateCluster").WithError(err).Error("invalid cluster root CA")
   126  			return status.Errorf(codes.Internal, "error loading cluster rootCA for update")
   127  		}
   128  
   129  		cluster.Meta.Version = *request.ClusterVersion
   130  		cluster.Spec = *request.Spec.Copy()
   131  
   132  		expireBlacklistedCerts(cluster)
   133  
   134  		if request.Rotation.WorkerJoinToken {
   135  			cluster.RootCA.JoinTokens.Worker = ca.GenerateJoinToken(&rootCA, cluster.FIPS)
   136  		}
   137  		if request.Rotation.ManagerJoinToken {
   138  			cluster.RootCA.JoinTokens.Manager = ca.GenerateJoinToken(&rootCA, cluster.FIPS)
   139  		}
   140  
   141  		updatedRootCA, err := validateCAConfig(ctx, s.securityConfig, cluster)
   142  		if err != nil {
   143  			return err
   144  		}
   145  		cluster.RootCA = *updatedRootCA
   146  
   147  		var unlockKeys []*api.EncryptionKey
   148  		var managerKey *api.EncryptionKey
   149  		for _, eKey := range cluster.UnlockKeys {
   150  			if eKey.Subsystem == ca.ManagerRole {
   151  				if !cluster.Spec.EncryptionConfig.AutoLockManagers {
   152  					continue
   153  				}
   154  				managerKey = eKey
   155  			}
   156  			unlockKeys = append(unlockKeys, eKey)
   157  		}
   158  
   159  		switch {
   160  		case !cluster.Spec.EncryptionConfig.AutoLockManagers:
   161  			break
   162  		case managerKey == nil:
   163  			unlockKeys = append(unlockKeys, &api.EncryptionKey{
   164  				Subsystem: ca.ManagerRole,
   165  				Key:       encryption.GenerateSecretKey(),
   166  			})
   167  		case request.Rotation.ManagerUnlockKey:
   168  			managerKey.Key = encryption.GenerateSecretKey()
   169  		}
   170  		cluster.UnlockKeys = unlockKeys
   171  
   172  		return store.UpdateCluster(tx, cluster)
   173  	})
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	redactedClusters := redactClusters([]*api.Cluster{cluster})
   179  
   180  	// WARN: we should never return cluster here. We need to redact the private fields first.
   181  	return &api.UpdateClusterResponse{
   182  		Cluster: redactedClusters[0],
   183  	}, nil
   184  }
   185  
   186  func filterClusters(candidates []*api.Cluster, filters ...func(*api.Cluster) bool) []*api.Cluster {
   187  	result := []*api.Cluster{}
   188  
   189  	for _, c := range candidates {
   190  		match := true
   191  		for _, f := range filters {
   192  			if !f(c) {
   193  				match = false
   194  				break
   195  			}
   196  		}
   197  		if match {
   198  			result = append(result, c)
   199  		}
   200  	}
   201  
   202  	return result
   203  }
   204  
   205  // ListClusters returns a list of all clusters.
   206  func (s *Server) ListClusters(ctx context.Context, request *api.ListClustersRequest) (*api.ListClustersResponse, error) {
   207  	var (
   208  		clusters []*api.Cluster
   209  		err      error
   210  	)
   211  	s.store.View(func(tx store.ReadTx) {
   212  		switch {
   213  		case request.Filters != nil && len(request.Filters.Names) > 0:
   214  			clusters, err = store.FindClusters(tx, buildFilters(store.ByName, request.Filters.Names))
   215  		case request.Filters != nil && len(request.Filters.NamePrefixes) > 0:
   216  			clusters, err = store.FindClusters(tx, buildFilters(store.ByNamePrefix, request.Filters.NamePrefixes))
   217  		case request.Filters != nil && len(request.Filters.IDPrefixes) > 0:
   218  			clusters, err = store.FindClusters(tx, buildFilters(store.ByIDPrefix, request.Filters.IDPrefixes))
   219  		default:
   220  			clusters, err = store.FindClusters(tx, store.All)
   221  		}
   222  	})
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	if request.Filters != nil {
   228  		clusters = filterClusters(clusters,
   229  			func(e *api.Cluster) bool {
   230  				return filterContains(e.Spec.Annotations.Name, request.Filters.Names)
   231  			},
   232  			func(e *api.Cluster) bool {
   233  				return filterContainsPrefix(e.Spec.Annotations.Name, request.Filters.NamePrefixes)
   234  			},
   235  			func(e *api.Cluster) bool {
   236  				return filterContainsPrefix(e.ID, request.Filters.IDPrefixes)
   237  			},
   238  			func(e *api.Cluster) bool {
   239  				return filterMatchLabels(e.Spec.Annotations.Labels, request.Filters.Labels)
   240  			},
   241  		)
   242  	}
   243  
   244  	// WARN: we should never return cluster here. We need to redact the private fields first.
   245  	return &api.ListClustersResponse{
   246  		Clusters: redactClusters(clusters),
   247  	}, nil
   248  }
   249  
   250  // redactClusters is a method that enforces a whitelist of fields that are ok to be
   251  // returned in the Cluster object. It should filter out all sensitive information.
   252  func redactClusters(clusters []*api.Cluster) []*api.Cluster {
   253  	var redactedClusters []*api.Cluster
   254  	// Only add public fields to the new clusters
   255  	for _, cluster := range clusters {
   256  		// Copy all the mandatory fields
   257  		// Do not copy secret keys
   258  		redactedSpec := cluster.Spec.Copy()
   259  		redactedSpec.CAConfig.SigningCAKey = nil
   260  		// the cert is not a secret, but if API users get the cluster spec and then update,
   261  		// then because the cert is included but not the key, the user can get update errors
   262  		// or unintended consequences (such as telling swarm to forget about the key so long
   263  		// as there is a corresponding external CA)
   264  		redactedSpec.CAConfig.SigningCACert = nil
   265  
   266  		redactedRootCA := cluster.RootCA.Copy()
   267  		redactedRootCA.CAKey = nil
   268  		if r := redactedRootCA.RootRotation; r != nil {
   269  			r.CAKey = nil
   270  		}
   271  		newCluster := &api.Cluster{
   272  			ID:                      cluster.ID,
   273  			Meta:                    cluster.Meta,
   274  			Spec:                    *redactedSpec,
   275  			RootCA:                  *redactedRootCA,
   276  			BlacklistedCertificates: cluster.BlacklistedCertificates,
   277  			DefaultAddressPool:      cluster.DefaultAddressPool,
   278  			SubnetSize:              cluster.SubnetSize,
   279  			VXLANUDPPort:            cluster.VXLANUDPPort,
   280  		}
   281  		if newCluster.DefaultAddressPool == nil {
   282  			// This is just for CLI display. Set the inbuilt default pool for
   283  			// user reference.
   284  			newCluster.DefaultAddressPool = inbuiltDefaultAddressPool
   285  			newCluster.SubnetSize = inbuiltSubnetSize
   286  		}
   287  		if newCluster.VXLANUDPPort == 0 {
   288  			newCluster.VXLANUDPPort = defaultVXLANPort
   289  		}
   290  		redactedClusters = append(redactedClusters, newCluster)
   291  	}
   292  
   293  	return redactedClusters
   294  }
   295  
   296  func expireBlacklistedCerts(cluster *api.Cluster) {
   297  	nowMinusGrace := time.Now().Add(-expiredCertGrace)
   298  
   299  	for cn, blacklistedCert := range cluster.BlacklistedCertificates {
   300  		if blacklistedCert.Expiry == nil {
   301  			continue
   302  		}
   303  
   304  		expiry, err := gogotypes.TimestampFromProto(blacklistedCert.Expiry)
   305  		if err == nil && nowMinusGrace.After(expiry) {
   306  			delete(cluster.BlacklistedCertificates, cn)
   307  		}
   308  	}
   309  }