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
}