github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/broker/instance_create.go (about) 1 package broker 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "net" 9 "net/http" 10 "net/netip" 11 "strings" 12 13 "github.com/kyma-project/kyma-environment-broker/internal/networking" 14 15 "github.com/hashicorp/go-multierror" 16 17 "github.com/kyma-project/kyma-environment-broker/internal/euaccess" 18 19 "k8s.io/client-go/tools/clientcmd" 20 21 "github.com/google/uuid" 22 "github.com/kyma-incubator/compass/components/director/pkg/jsonschema" 23 "github.com/pivotal-cf/brokerapi/v8/domain" 24 "github.com/pivotal-cf/brokerapi/v8/domain/apiresponses" 25 "github.com/sirupsen/logrus" 26 27 "github.com/kyma-project/kyma-environment-broker/common/gardener" 28 "github.com/kyma-project/kyma-environment-broker/internal" 29 "github.com/kyma-project/kyma-environment-broker/internal/dashboard" 30 "github.com/kyma-project/kyma-environment-broker/internal/middleware" 31 "github.com/kyma-project/kyma-environment-broker/internal/ptr" 32 "github.com/kyma-project/kyma-environment-broker/internal/storage" 33 "github.com/kyma-project/kyma-environment-broker/internal/storage/dberr" 34 ) 35 36 //go:generate mockery --name=Queue --output=automock --outpkg=automock --case=underscore 37 //go:generate mockery --name=PlanValidator --output=automock --outpkg=automock --case=underscore 38 39 type ( 40 Queue interface { 41 Add(operationId string) 42 } 43 44 PlanValidator interface { 45 IsPlanSupport(planID string) bool 46 } 47 ) 48 49 type ProvisionEndpoint struct { 50 config Config 51 operationsStorage storage.Operations 52 instanceStorage storage.Instances 53 queue Queue 54 builderFactory PlanValidator 55 enabledPlanIDs map[string]struct{} 56 plansConfig PlansConfig 57 kymaVerOnDemand bool 58 planDefaults PlanDefaults 59 60 shootDomain string 61 shootProject string 62 shootDnsProviders gardener.DNSProvidersData 63 64 dashboardConfig dashboard.Config 65 66 euAccessWhitelist euaccess.WhitelistSet 67 euAccessRejectionMessage string 68 69 log logrus.FieldLogger 70 } 71 72 func NewProvision(cfg Config, 73 gardenerConfig gardener.Config, 74 operationsStorage storage.Operations, 75 instanceStorage storage.Instances, 76 queue Queue, 77 builderFactory PlanValidator, 78 plansConfig PlansConfig, 79 kvod bool, 80 planDefaults PlanDefaults, 81 euAccessWhitelist euaccess.WhitelistSet, 82 euRejectMessage string, 83 log logrus.FieldLogger, 84 dashboardConfig dashboard.Config, 85 ) *ProvisionEndpoint { 86 enabledPlanIDs := map[string]struct{}{} 87 for _, planName := range cfg.EnablePlans { 88 id := PlanIDsMapping[planName] 89 enabledPlanIDs[id] = struct{}{} 90 } 91 92 return &ProvisionEndpoint{ 93 config: cfg, 94 operationsStorage: operationsStorage, 95 instanceStorage: instanceStorage, 96 queue: queue, 97 builderFactory: builderFactory, 98 log: log.WithField("service", "ProvisionEndpoint"), 99 enabledPlanIDs: enabledPlanIDs, 100 plansConfig: plansConfig, 101 kymaVerOnDemand: kvod, 102 shootDomain: gardenerConfig.ShootDomain, 103 shootProject: gardenerConfig.Project, 104 shootDnsProviders: gardenerConfig.DNSProviders, 105 planDefaults: planDefaults, 106 euAccessWhitelist: euAccessWhitelist, 107 euAccessRejectionMessage: euRejectMessage, 108 dashboardConfig: dashboardConfig, 109 } 110 } 111 112 // Provision creates a new service instance 113 // 114 // PUT /v2/service_instances/{instance_id} 115 func (b *ProvisionEndpoint) Provision(ctx context.Context, instanceID string, details domain.ProvisionDetails, asyncAllowed bool) (domain.ProvisionedServiceSpec, error) { 116 operationID := uuid.New().String() 117 logger := b.log.WithFields(logrus.Fields{"instanceID": instanceID, "operationID": operationID, "planID": details.PlanID}) 118 logger.Infof("Provision called with context: %s", marshallRawContext(hideSensitiveDataFromRawContext(details.RawContext))) 119 120 region, found := middleware.RegionFromContext(ctx) 121 if !found { 122 err := fmt.Errorf("No region specified in request.") 123 return domain.ProvisionedServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusInternalServerError, "provisioning") 124 } 125 platformProvider, found := middleware.ProviderFromContext(ctx) 126 if !found { 127 err := fmt.Errorf("No provider specified in request.") 128 return domain.ProvisionedServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusInternalServerError, "provisioning") 129 } 130 131 // validation of incoming input 132 ersContext, parameters, err := b.validateAndExtract(details, platformProvider, ctx, logger) 133 if err != nil { 134 errMsg := fmt.Sprintf("[instanceID: %s] %s", instanceID, err) 135 return domain.ProvisionedServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusBadRequest, errMsg) 136 } 137 138 provisioningParameters := internal.ProvisioningParameters{ 139 PlanID: details.PlanID, 140 ServiceID: details.ServiceID, 141 ErsContext: ersContext, 142 Parameters: parameters, 143 PlatformRegion: region, 144 PlatformProvider: platformProvider, 145 } 146 147 logger.Infof("Starting provisioning runtime: Name=%s, GlobalAccountID=%s, SubAccountID=%s PlatformRegion=%s, ProvisioningParameterts.Region=%s, ProvisioningParameterts.MachineType=%s", 148 parameters.Name, ersContext.GlobalAccountID, ersContext.SubAccountID, region, valueOfPtr(parameters.Region), valueOfPtr(parameters.MachineType)) 149 logParametersWithMaskedKubeconfig(parameters, logger) 150 151 // check if operation with instance ID already created 152 existingOperation, errStorage := b.operationsStorage.GetProvisioningOperationByInstanceID(instanceID) 153 switch { 154 case errStorage != nil && !dberr.IsNotFound(errStorage): 155 logger.Errorf("cannot get existing operation from storage %s", errStorage) 156 return domain.ProvisionedServiceSpec{}, fmt.Errorf("cannot get existing operation from storage") 157 case existingOperation != nil && !dberr.IsNotFound(errStorage): 158 return b.handleExistingOperation(existingOperation, provisioningParameters) 159 } 160 161 shootName := gardener.CreateShootName() 162 shootDomainSuffix := strings.Trim(b.shootDomain, ".") 163 164 dashboardURL := b.createDashboardURL(details.PlanID, instanceID) 165 166 // create and save new operation 167 operation, err := internal.NewProvisioningOperationWithID(operationID, instanceID, provisioningParameters) 168 if err != nil { 169 logger.Errorf("cannot create new operation: %s", err) 170 return domain.ProvisionedServiceSpec{}, fmt.Errorf("cannot create new operation") 171 } 172 173 operation.ShootName = shootName 174 operation.ShootDomain = fmt.Sprintf("%s.%s", shootName, shootDomainSuffix) 175 operation.ShootDNSProviders = b.shootDnsProviders 176 operation.DashboardURL = dashboardURL 177 // for own cluster plan - KEB uses provided shoot name and shoot domain 178 if IsOwnClusterPlan(provisioningParameters.PlanID) { 179 operation.ShootName = provisioningParameters.Parameters.ShootName 180 operation.ShootDomain = provisioningParameters.Parameters.ShootDomain 181 } 182 logger.Infof("Runtime ShootDomain: %s", operation.ShootDomain) 183 184 err = b.operationsStorage.InsertOperation(operation.Operation) 185 if err != nil { 186 logger.Errorf("cannot save operation: %s", err) 187 return domain.ProvisionedServiceSpec{}, fmt.Errorf("cannot save operation") 188 } 189 190 instance := internal.Instance{ 191 InstanceID: instanceID, 192 GlobalAccountID: ersContext.GlobalAccountID, 193 SubAccountID: ersContext.SubAccountID, 194 ServiceID: provisioningParameters.ServiceID, 195 ServiceName: KymaServiceName, 196 ServicePlanID: provisioningParameters.PlanID, 197 ServicePlanName: PlanNamesMapping[provisioningParameters.PlanID], 198 DashboardURL: dashboardURL, 199 Parameters: operation.ProvisioningParameters, 200 } 201 err = b.instanceStorage.Insert(instance) 202 if err != nil { 203 logger.Errorf("cannot save instance in storage: %s", err) 204 return domain.ProvisionedServiceSpec{}, fmt.Errorf("cannot save instance") 205 } 206 207 logger.Info("Adding operation to provisioning queue") 208 b.queue.Add(operation.ID) 209 210 return domain.ProvisionedServiceSpec{ 211 IsAsync: true, 212 OperationData: operation.ID, 213 DashboardURL: dashboardURL, 214 Metadata: domain.InstanceMetadata{ 215 Labels: ResponseLabels(operation, instance, b.config.URL, b.config.EnableKubeconfigURLLabel), 216 }, 217 }, nil 218 } 219 220 func logParametersWithMaskedKubeconfig(parameters internal.ProvisioningParametersDTO, logger *logrus.Entry) { 221 parameters.Kubeconfig = "*****" 222 logger.Infof("Runtime parameters: %+v", parameters) 223 } 224 225 func valueOfPtr(ptr *string) string { 226 if ptr == nil { 227 return "" 228 } 229 return *ptr 230 } 231 232 func (b *ProvisionEndpoint) validateAndExtract(details domain.ProvisionDetails, provider internal.CloudProvider, ctx context.Context, l logrus.FieldLogger) (internal.ERSContext, internal.ProvisioningParametersDTO, error) { 233 var ersContext internal.ERSContext 234 var parameters internal.ProvisioningParametersDTO 235 236 if details.ServiceID != KymaServiceID { 237 return ersContext, parameters, fmt.Errorf("service_id not recognized") 238 } 239 if _, exists := b.enabledPlanIDs[details.PlanID]; !exists { 240 return ersContext, parameters, fmt.Errorf("plan ID %q is not recognized", details.PlanID) 241 } 242 243 ersContext, err := b.extractERSContext(details) 244 logger := l.WithField("globalAccountID", ersContext.GlobalAccountID) 245 if err != nil { 246 return ersContext, parameters, fmt.Errorf("while extracting ers context: %w", err) 247 } 248 249 parameters, err = b.extractInputParameters(details) 250 if err != nil { 251 return ersContext, parameters, fmt.Errorf("while extracting input parameters: %w", err) 252 } 253 defaults, err := b.planDefaults(details.PlanID, provider, parameters.Provider) 254 if err != nil { 255 return ersContext, parameters, fmt.Errorf("while obtaining plan defaults: %w", err) 256 } 257 258 // TODO: remove when the feature (networking params) is completed and tested on prod 259 if !b.config.AllowNetworkingParameters && parameters.Networking != nil { 260 return ersContext, parameters, fmt.Errorf("providing networking parameters is not allowed") 261 } 262 if err := b.validateNetworking(parameters); err != nil { 263 return ersContext, parameters, err 264 } 265 266 if !b.config.AllowModulesParameters { 267 b.log.Infof("modules section passed to API, but AllowModulesParameters is set to false. Parameters will be reset to nil") 268 parameters.Modules = nil 269 } 270 271 var autoscalerMin, autoscalerMax int 272 if defaults.GardenerConfig != nil { 273 p := defaults.GardenerConfig 274 autoscalerMin, autoscalerMax = p.AutoScalerMin, p.AutoScalerMax 275 } 276 if err := parameters.AutoScalerParameters.Validate(autoscalerMin, autoscalerMax); err != nil { 277 return ersContext, parameters, apiresponses.NewFailureResponse(err, http.StatusUnprocessableEntity, err.Error()) 278 } 279 if parameters.OIDC.IsProvided() { 280 if err := parameters.OIDC.Validate(); err != nil { 281 return ersContext, parameters, apiresponses.NewFailureResponse(err, http.StatusUnprocessableEntity, err.Error()) 282 } 283 } 284 285 planValidator, err := b.validator(&details, provider, ctx) 286 if err != nil { 287 return ersContext, parameters, fmt.Errorf("while creating plan validator: %w", err) 288 } 289 result, err := planValidator.ValidateString(string(details.RawParameters)) 290 if err != nil { 291 return ersContext, parameters, fmt.Errorf("while executing JSON schema validator: %w", err) 292 } 293 if !result.Valid { 294 return ersContext, parameters, fmt.Errorf("while validating input parameters: %w", result.Error) 295 } 296 297 // EU Access: reject requests for not whitelisted globalAccountIds 298 if isEuRestrictedAccess(ctx) { 299 logger.Infof("EU Access restricted instance creation") 300 if euaccess.IsNotWhitelisted(ersContext.GlobalAccountID, b.euAccessWhitelist) { 301 logger.Infof(b.euAccessRejectionMessage) 302 err = fmt.Errorf(b.euAccessRejectionMessage) 303 return ersContext, parameters, apiresponses.NewFailureResponse(err, http.StatusBadRequest, "provisioning") 304 } 305 } 306 307 if !b.kymaVerOnDemand { 308 logger.Infof("Kyma on demand functionality is disabled. Default Kyma version will be used instead %s", parameters.KymaVersion) 309 parameters.KymaVersion = "" 310 parameters.OverridesVersion = "" 311 } 312 parameters.LicenceType = b.determineLicenceType(details.PlanID) 313 314 found := b.builderFactory.IsPlanSupport(details.PlanID) 315 if !found { 316 return ersContext, parameters, fmt.Errorf("the plan ID not known, planID: %s", details.PlanID) 317 } 318 319 if IsOwnClusterPlan(details.PlanID) { 320 decodedKubeconfig, err := base64.StdEncoding.DecodeString(parameters.Kubeconfig) 321 if err != nil { 322 return ersContext, parameters, fmt.Errorf("while decoding kubeconfig: %w", err) 323 } 324 parameters.Kubeconfig = string(decodedKubeconfig) 325 err = validateKubeconfig(parameters.Kubeconfig) 326 if err != nil { 327 return ersContext, parameters, fmt.Errorf("while validating kubeconfig: %w", err) 328 } 329 } 330 331 if IsTrialPlan(details.PlanID) && parameters.Region != nil && *parameters.Region != "" { 332 _, valid := validRegionsForTrial[TrialCloudRegion(*parameters.Region)] 333 if !valid { 334 return ersContext, parameters, fmt.Errorf("invalid region specified in request for trial") 335 } 336 } 337 338 if IsTrialPlan(details.PlanID) && b.config.OnlySingleTrialPerGA { 339 count, err := b.instanceStorage.GetNumberOfInstancesForGlobalAccountID(ersContext.GlobalAccountID) 340 if err != nil { 341 return ersContext, parameters, fmt.Errorf("while checking if a trial Kyma instance exists for given global account: %w", err) 342 } 343 344 if count > 0 { 345 logger.Info("Provisioning Trial SKR rejected, such instance was already created for this Global Account") 346 return ersContext, parameters, fmt.Errorf("trial Kyma was created for the global account, but there is only one allowed") 347 } 348 } 349 350 return ersContext, parameters, nil 351 } 352 353 func isEuRestrictedAccess(ctx context.Context) bool { 354 platformRegion, _ := middleware.RegionFromContext(ctx) 355 return euaccess.IsEURestrictedAccess(platformRegion) 356 } 357 358 // Rudimentary kubeconfig validation 359 func validateKubeconfig(kubeconfig string) error { 360 config, err := clientcmd.Load([]byte(kubeconfig)) 361 if err != nil { 362 return err 363 } 364 err = clientcmd.Validate(*config) 365 if err != nil { 366 return err 367 } 368 return nil 369 } 370 371 func (b *ProvisionEndpoint) extractERSContext(details domain.ProvisionDetails) (internal.ERSContext, error) { 372 var ersContext internal.ERSContext 373 err := json.Unmarshal(details.RawContext, &ersContext) 374 if err != nil { 375 return ersContext, fmt.Errorf("while decoding context: %w", err) 376 } 377 378 if ersContext.GlobalAccountID == "" { 379 return ersContext, fmt.Errorf("global accountID parameter cannot be empty") 380 } 381 if ersContext.SubAccountID == "" { 382 return ersContext, fmt.Errorf("subAccountID parameter cannot be empty") 383 } 384 if ersContext.UserID == "" { 385 return ersContext, fmt.Errorf("UserID parameter cannot be empty") 386 } 387 ersContext.UserID = strings.ToLower(ersContext.UserID) 388 389 return ersContext, nil 390 } 391 392 func (b *ProvisionEndpoint) extractInputParameters(details domain.ProvisionDetails) (internal.ProvisioningParametersDTO, error) { 393 var parameters internal.ProvisioningParametersDTO 394 err := json.Unmarshal(details.RawParameters, ¶meters) 395 if err != nil { 396 return parameters, fmt.Errorf("while unmarshaling raw parameters: %w", err) 397 } 398 399 return parameters, nil 400 } 401 402 func (b *ProvisionEndpoint) handleExistingOperation(operation *internal.ProvisioningOperation, input internal.ProvisioningParameters) (domain.ProvisionedServiceSpec, error) { 403 404 if !operation.ProvisioningParameters.IsEqual(input) { 405 err := fmt.Errorf("provisioning operation already exist") 406 msg := fmt.Sprintf("provisioning operation with InstanceID %s already exist", operation.InstanceID) 407 return domain.ProvisionedServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusConflict, msg) 408 } 409 410 instance, err := b.instanceStorage.GetByID(operation.InstanceID) 411 if err != nil { 412 err := fmt.Errorf("cannot fetch instance for operation") 413 msg := fmt.Sprintf("cannot fetch instance with ID: %s for operation woth ID: %s", operation.InstanceID, operation.ID) 414 return domain.ProvisionedServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusConflict, msg) 415 } 416 417 return domain.ProvisionedServiceSpec{ 418 IsAsync: true, 419 OperationData: operation.ID, 420 DashboardURL: operation.DashboardURL, 421 Metadata: domain.InstanceMetadata{ 422 Labels: ResponseLabels(*operation, *instance, b.config.URL, b.config.EnableKubeconfigURLLabel), 423 }, 424 }, nil 425 } 426 427 func (b *ProvisionEndpoint) determineLicenceType(planId string) *string { 428 if planId == AzureLitePlanID || IsTrialPlan(planId) { 429 return ptr.String(internal.LicenceTypeLite) 430 } 431 432 return nil 433 } 434 435 func (b *ProvisionEndpoint) validator(details *domain.ProvisionDetails, provider internal.CloudProvider, ctx context.Context) (JSONSchemaValidator, error) { 436 platformRegion, _ := middleware.RegionFromContext(ctx) 437 plans := Plans(b.plansConfig, provider, b.config.IncludeAdditionalParamsInSchema, euaccess.IsEURestrictedAccess(platformRegion), b.config.RegionParameterIsRequired, b.config.AllowModulesParameters) 438 plan := plans[details.PlanID] 439 schema := string(Marshal(plan.Schemas.Instance.Create.Parameters)) 440 441 return jsonschema.NewValidatorFromStringSchema(schema) 442 } 443 444 func (b *ProvisionEndpoint) createDashboardURL(planID, instanceID string) string { 445 if IsOwnClusterPlan(planID) { 446 return b.dashboardConfig.LandscapeURL 447 } else { 448 return fmt.Sprintf("%s/?kubeconfigID=%s", b.dashboardConfig.LandscapeURL, instanceID) 449 } 450 } 451 452 func validateCidr(cidr string) (*net.IPNet, error) { 453 ip, ipNet, err := net.ParseCIDR(cidr) 454 if err != nil { 455 return nil, err 456 } 457 // find cases like: 10.250.0.1/19 458 if ipNet != nil { 459 if !ipNet.IP.Equal(ip) { 460 return nil, fmt.Errorf("%s must be valid canonical CIDR", ip) 461 } 462 } 463 return ipNet, nil 464 } 465 466 func (b *ProvisionEndpoint) validateNetworking(parameters internal.ProvisioningParametersDTO) error { 467 var err, e error 468 if len(parameters.Zones) > 4 { 469 // the algorithm of creating AWS zone CIDRs does not work for more than 4 zones 470 err = multierror.Append(err, fmt.Errorf("number of zones must not be greater than 4")) 471 } 472 if parameters.Networking == nil { 473 return nil 474 } 475 476 // currently we do not support Pod's and Service's 477 if parameters.Networking.PodsCidr != nil { 478 return fmt.Errorf("pod network's CIDR is not supported in the request") 479 } 480 if parameters.Networking.ServicesCidr != nil { 481 return fmt.Errorf("service network's CIDR is not supported in the request") 482 } 483 484 var nodes, services, pods *net.IPNet 485 if nodes, e = validateCidr(parameters.Networking.NodesCidr); e != nil { 486 err = multierror.Append(err, fmt.Errorf("while parsing nodes CIDR: %w", e)) 487 } 488 // error is handled before, in the validate CIDR 489 cidr, _ := netip.ParsePrefix(parameters.Networking.NodesCidr) 490 if cidr.Bits() > 23 { 491 err = multierror.Append(err, fmt.Errorf("the suffix of the node CIDR must not be greater than 26")) 492 } 493 494 if err != nil { 495 return err 496 } 497 498 for _, seed := range networking.GardenerSeedCIDRs { 499 _, seedCidr, _ := net.ParseCIDR(seed) 500 if e := validateOverlapping(*nodes, *seedCidr); e != nil { 501 err = multierror.Append(err, fmt.Errorf("nodes CIDR must not overlap %s", seed)) 502 } 503 } 504 505 if parameters.Networking.PodsCidr != nil { 506 if pods, e = validateCidr(*parameters.Networking.PodsCidr); e != nil { 507 err = multierror.Append(err, fmt.Errorf("while parsing pods CIDR: %w", e)) 508 } 509 } else { 510 _, pods, _ = net.ParseCIDR(networking.DefaultPodsCIDR) 511 } 512 if parameters.Networking.ServicesCidr != nil { 513 if services, e = validateCidr(*parameters.Networking.ServicesCidr); e != nil { 514 err = multierror.Append(err, fmt.Errorf("while parsing services CIDR: %w", e)) 515 } 516 } else { 517 _, services, _ = net.ParseCIDR(networking.DefaultServicesCIDR) 518 } 519 if err != nil { 520 return err 521 } 522 523 if e := validateOverlapping(*nodes, *pods); e != nil { 524 err = multierror.Append(err, fmt.Errorf("nodes CIDR must not overlap %s", pods.String())) 525 } 526 if e := validateOverlapping(*nodes, *services); e != nil { 527 err = multierror.Append(err, fmt.Errorf("nodes CIDR must not overlap %s", services.String())) 528 } 529 if e := validateOverlapping(*services, *pods); e != nil { 530 err = multierror.Append(err, fmt.Errorf("services CIDR must not overlap pods CIDR")) 531 } 532 533 return err 534 } 535 536 func validateOverlapping(n1 net.IPNet, n2 net.IPNet) error { 537 538 if n1.Contains(n2.IP) || n2.Contains(n1.IP) { 539 return fmt.Errorf("%s overlaps %s", n1.String(), n2.String()) 540 } 541 542 return nil 543 }