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 }