func()

in pkg/frontend/openshiftcluster_putorpatch.go [95:353]


func (f *frontend) _putOrPatchOpenShiftCluster(ctx context.Context, log *logrus.Entry, putOrPatchClusterParameters PutOrPatchOpenshiftClusterParameters) ([]byte, error) {
	subscription, err := f.validateSubscriptionState(ctx, putOrPatchClusterParameters.path, api.SubscriptionStateRegistered)
	if err != nil {
		return nil, err
	}

	dbOpenShiftClusters, err := f.dbGroup.OpenShiftClusters()
	if err != nil {
		return nil, err
	}

	doc, err := dbOpenShiftClusters.Get(ctx, putOrPatchClusterParameters.path)
	if err != nil && !cosmosdb.IsErrorStatusCode(err, http.StatusNotFound) {
		return nil, err
	}
	isCreate := doc == nil

	if isCreate {
		originalR, err := azure.ParseResourceID(putOrPatchClusterParameters.originalPath)
		if err != nil {
			return nil, err
		}

		doc = &api.OpenShiftClusterDocument{
			ID:  dbOpenShiftClusters.NewUUID(),
			Key: putOrPatchClusterParameters.path,
			OpenShiftCluster: &api.OpenShiftCluster{
				ID:   putOrPatchClusterParameters.originalPath,
				Name: originalR.ResourceName,
				Type: originalR.Provider + "/" + originalR.ResourceType,
				Properties: api.OpenShiftClusterProperties{
					ArchitectureVersion: version.InstallArchitectureVersion,
					ProvisioningState:   api.ProvisioningStateSucceeded,
					CreatedAt:           f.now().UTC(),
					CreatedBy:           version.GitCommit,
					ProvisionedBy:       version.GitCommit,
				},
			},
		}

		if !f.env.IsLocalDevelopmentMode() /* not local dev or CI */ {
			doc.OpenShiftCluster.Properties.FeatureProfile.GatewayEnabled = true
		}
	}

	doc.CorrelationData = putOrPatchClusterParameters.correlationData

	err = validateTerminalProvisioningState(doc.OpenShiftCluster.Properties.ProvisioningState)
	if err != nil {
		return nil, err
	}

	if doc.OpenShiftCluster.Properties.ProvisioningState == api.ProvisioningStateFailed {
		switch doc.OpenShiftCluster.Properties.FailedProvisioningState {
		case api.ProvisioningStateCreating:
			return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeRequestNotAllowed, "", "Request is not allowed on cluster whose creation failed. Delete the cluster.")
		case api.ProvisioningStateUpdating:
			// allow: a previous failure to update should not prevent a new
			// operation.
		case api.ProvisioningStateDeleting:
			return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeRequestNotAllowed, "", "Request is not allowed on cluster whose deletion failed. Delete the cluster.")
		default:
			return nil, fmt.Errorf("unexpected failedProvisioningState %q", doc.OpenShiftCluster.Properties.FailedProvisioningState)
		}
	}

	// If Put or Patch is executed we will enrich document with cluster data.
	if !isCreate {
		timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
		defer cancel()

		f.clusterEnricher.Enrich(timeoutCtx, log, doc.OpenShiftCluster)
	}

	var ext interface{}
	switch putOrPatchClusterParameters.method {
	// In case of PUT we will take customer request payload and store into database
	// Our base structure for unmarshal is skeleton document with values we
	// think is required. We expect payload to have everything else required.
	case http.MethodPut:
		document := &api.OpenShiftCluster{
			ID:   doc.OpenShiftCluster.ID,
			Name: doc.OpenShiftCluster.Name,
			Type: doc.OpenShiftCluster.Type,
			Properties: api.OpenShiftClusterProperties{
				ProvisioningState: doc.OpenShiftCluster.Properties.ProvisioningState,
				ClusterProfile: api.ClusterProfile{
					PullSecret: doc.OpenShiftCluster.Properties.ClusterProfile.PullSecret,
					Version:    doc.OpenShiftCluster.Properties.ClusterProfile.Version,
				},
			},
			SystemData: doc.OpenShiftCluster.SystemData,
		}

		if doc.OpenShiftCluster.Properties.ServicePrincipalProfile != nil {
			document.Properties.ServicePrincipalProfile = &api.ServicePrincipalProfile{}
			document.Properties.ServicePrincipalProfile.ClientSecret = doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientSecret
		}

		ext = putOrPatchClusterParameters.converter.ToExternal(document)

	// In case of PATCH we take current cluster document, which is enriched
	// from the cluster and use it as base for unmarshal. So customer can
	// provide single field json to be updated in the database.
	// Patch should be used for updating individual fields of the document.
	case http.MethodPatch:
		if putOrPatchClusterParameters.apiVersion == admin.APIVersion {
			// OperatorFlagsMergeStrategy==reset will place the default flags in
			// the external object and then merge in the body's flags when the
			// request is unmarshaled below.
			err = admin.OperatorFlagsMergeStrategy(doc.OpenShiftCluster, putOrPatchClusterParameters.body)
			if err != nil {
				// OperatorFlagsMergeStrategy returns CloudErrors
				return nil, err
			}
		}
		ext = putOrPatchClusterParameters.converter.ToExternal(doc.OpenShiftCluster)
	}

	putOrPatchClusterParameters.converter.ExternalNoReadOnly(ext)

	err = json.Unmarshal(putOrPatchClusterParameters.body, &ext)
	if err != nil {
		return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidRequestContent, "", fmt.Sprintf("The request content was invalid and could not be deserialized: %q.", err))
	}

	if isCreate {
		putOrPatchClusterParameters.converter.ToInternal(ext, doc.OpenShiftCluster)
		err = f.ValidateNewCluster(ctx, subscription, doc.OpenShiftCluster, putOrPatchClusterParameters.staticValidator, ext, putOrPatchClusterParameters.path)
		if err != nil {
			return nil, err
		}
	} else {
		err = putOrPatchClusterParameters.staticValidator.Static(ext, doc.OpenShiftCluster, f.env.Location(), f.env.Domain(), f.env.FeatureIsSet(env.FeatureRequireD2sWorkers), putOrPatchClusterParameters.path)
		if err != nil {
			return nil, err
		}
	}

	oldID, oldName, oldType, oldSystemData := doc.OpenShiftCluster.ID, doc.OpenShiftCluster.Name, doc.OpenShiftCluster.Type, doc.OpenShiftCluster.SystemData
	putOrPatchClusterParameters.converter.ToInternal(ext, doc.OpenShiftCluster)
	doc.OpenShiftCluster.ID, doc.OpenShiftCluster.Name, doc.OpenShiftCluster.Type, doc.OpenShiftCluster.SystemData = oldID, oldName, oldType, oldSystemData

	// This will update systemData from the values in the header. Old values, which
	// is not provided in the header must be preserved
	f.systemDataClusterDocEnricher(doc, putOrPatchClusterParameters.systemData)

	if doc.OpenShiftCluster.UsesWorkloadIdentity() {
		if err := f.validatePlatformWorkloadIdentities(doc.OpenShiftCluster); err != nil {
			return nil, err
		}
	}

	if isCreate {
		err = f.validateInstallVersion(ctx, doc.OpenShiftCluster)
		if err != nil {
			return nil, err
		}

		// on create, make the cluster resourcegroup ID lower case to work
		// around LB/PLS bug
		doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID = strings.ToLower(doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID)

		doc.ClusterResourceGroupIDKey = strings.ToLower(doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID)

		// doc.ClientIDKey is used as part of the Cosmos DB instance's unique key policy. Because we have
		// one Cosmos DB instance per region, this value must be unique within the region.
		//
		// This effectively enforces:
		//   - Among all service principal clusters within a region, the service principal must be unique
		//   - Among all workload identity clusters within a region, the cluster MSI must be unique
		//
		// The name "clientIdKey" is an artifact of the world before workload identity where there were
		// only service principal clusters.
		if doc.OpenShiftCluster.UsesWorkloadIdentity() {
			clusterMsiResourceId, err := doc.OpenShiftCluster.ClusterMsiResourceId()
			if err != nil {
				return nil, err
			}

			doc.ClientIDKey = strings.ToLower(clusterMsiResourceId.String())
		} else {
			doc.ClientIDKey = strings.ToLower(doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientID)
		}

		doc.OpenShiftCluster.Properties.ProvisioningState = api.ProvisioningStateCreating

		// Persist identity URL and tenant ID only for managed/workload identity cluster create
		// We don't support updating cluster managed identity after cluster creation
		if doc.OpenShiftCluster.UsesWorkloadIdentity() {
			if err := validateIdentityUrl(doc.OpenShiftCluster, putOrPatchClusterParameters.identityURL); err != nil {
				return nil, err
			}
			if err := validateIdentityTenantID(doc.OpenShiftCluster, putOrPatchClusterParameters.identityTenantID); err != nil {
				return nil, err
			}
		}

		doc.Bucket, err = f.bucketAllocator.Allocate()
		if err != nil {
			return nil, err
		}
	} else {
		setUpdateProvisioningState(doc, putOrPatchClusterParameters.apiVersion)
	}

	// SetDefaults will set defaults on cluster document
	api.SetDefaults(doc, operator.DefaultOperatorFlags)

	doc.AsyncOperationID, err = f.newAsyncOperation(ctx, putOrPatchClusterParameters.subId, putOrPatchClusterParameters.resourceProviderNamespace, doc)
	if err != nil {
		return nil, err
	}

	u, err := url.Parse(putOrPatchClusterParameters.referer)
	if err != nil {
		return nil, err
	}

	u.Path = f.operationsPath(putOrPatchClusterParameters.subId, putOrPatchClusterParameters.resourceProviderNamespace, doc.AsyncOperationID)
	*putOrPatchClusterParameters.header = http.Header{
		"Azure-AsyncOperation": []string{u.String()},
	}

	if isCreate {
		newdoc, err := dbOpenShiftClusters.Create(ctx, doc)
		if cosmosdb.IsErrorStatusCode(err, http.StatusPreconditionFailed) {
			return nil, f.validateOpenShiftUniqueKey(ctx, doc)
		}
		doc = newdoc
	} else {
		doc, err = dbOpenShiftClusters.Update(ctx, doc)
	}
	if err != nil {
		return nil, err
	}

	// We remove sensitive data from document to prevent sensitive data being
	// returned to the customer.
	doc.OpenShiftCluster.Properties.ClusterProfile.PullSecret = ""

	if doc.OpenShiftCluster.Properties.ServicePrincipalProfile != nil {
		doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientSecret = ""
	}
	doc.OpenShiftCluster.Properties.ClusterProfile.BoundServiceAccountSigningKey = nil

	// We don't return enriched worker profile data on PUT/PATCH operations
	doc.OpenShiftCluster.Properties.WorkerProfilesStatus = nil

	b, err := json.MarshalIndent(putOrPatchClusterParameters.converter.ToExternal(doc.OpenShiftCluster), "", "    ")
	if err != nil {
		return nil, err
	}

	if isCreate {
		err = statusCodeError(http.StatusCreated)
	}
	return b, err
}