in frontend/pkg/frontend/frontend.go [371:621]
func (f *Frontend) ArmResourceCreateOrUpdate(writer http.ResponseWriter, request *http.Request) {
var err error
// This handles both PUT and PATCH requests. PATCH requests will
// never create a new resource. The only other notable difference
// is the target struct that request bodies are overlayed onto:
//
// PUT requests overlay the request body onto a default resource
// struct, which only has API-specified non-zero default values.
// This means all required properties must be specified in the
// request body, whether creating or updating a resource.
//
// PATCH requests overlay the request body onto a resource struct
// that represents an existing resource to be updated.
ctx := request.Context()
logger := LoggerFromContext(ctx)
versionedInterface, err := VersionFromContext(ctx)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
resourceID, err := ResourceIDFromContext(ctx)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
systemData, err := SystemDataFromContext(ctx)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
resourceDoc, err := f.dbClient.GetResourceDoc(ctx, resourceID)
if err != nil && !errors.Is(err, database.ErrNotFound) {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
var updating = (resourceDoc != nil)
var operationRequest database.OperationRequest
var versionedCurrentCluster api.VersionedHCPOpenShiftCluster
var versionedRequestCluster api.VersionedHCPOpenShiftCluster
var successStatusCode int
if updating {
csCluster, err := f.clusterServiceClient.GetCluster(ctx, resourceDoc.InternalID)
if err != nil {
logger.Error(fmt.Sprintf("failed to fetch CS cluster for %s: %v", resourceID, err))
arm.WriteCloudError(writer, CSErrorToCloudError(err, resourceID))
return
}
hcpCluster := ConvertCStoHCPOpenShiftCluster(resourceID, csCluster)
// Do not set the TrackedResource.Tags field here. We need
// the Tags map to remain nil so we can see if the request
// body included a new set of resource tags.
operationRequest = database.OperationRequestUpdate
// This is slightly repetitive for the sake of clarity on PUT vs PATCH.
switch request.Method {
case http.MethodPut:
versionedCurrentCluster = versionedInterface.NewHCPOpenShiftCluster(hcpCluster)
versionedRequestCluster = versionedInterface.NewHCPOpenShiftCluster(nil)
successStatusCode = http.StatusOK
case http.MethodPatch:
versionedCurrentCluster = versionedInterface.NewHCPOpenShiftCluster(hcpCluster)
versionedRequestCluster = versionedInterface.NewHCPOpenShiftCluster(hcpCluster)
successStatusCode = http.StatusAccepted
}
} else {
operationRequest = database.OperationRequestCreate
switch request.Method {
case http.MethodPut:
versionedCurrentCluster = versionedInterface.NewHCPOpenShiftCluster(nil)
versionedRequestCluster = versionedInterface.NewHCPOpenShiftCluster(nil)
successStatusCode = http.StatusCreated
case http.MethodPatch:
// PATCH requests never create a new resource.
logger.Error("Resource not found")
arm.WriteResourceNotFoundError(writer, resourceID)
return
}
resourceDoc = database.NewResourceDocument(resourceID)
}
// CheckForProvisioningStateConflict does not log conflict errors
// but does log unexpected errors like database failures.
cloudError := f.CheckForProvisioningStateConflict(ctx, operationRequest, resourceDoc)
if cloudError != nil {
arm.WriteCloudError(writer, cloudError)
return
}
body, err := BodyFromContext(ctx)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
if err = json.Unmarshal(body, versionedRequestCluster); err != nil {
logger.Error(err.Error())
arm.WriteInvalidRequestContentError(writer, err)
return
}
cloudError = versionedRequestCluster.ValidateStatic(versionedCurrentCluster, updating, request.Method)
if cloudError != nil {
logger.Error(cloudError.Error())
arm.WriteCloudError(writer, cloudError)
return
}
hcpCluster := api.NewDefaultHCPOpenShiftCluster()
versionedRequestCluster.Normalize(hcpCluster)
hcpCluster.Name = request.PathValue(PathSegmentResourceName)
csCluster, err := f.BuildCSCluster(resourceID, request.Header, hcpCluster, updating)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
if updating {
logger.Info(fmt.Sprintf("updating resource %s", resourceID))
csCluster, err = f.clusterServiceClient.UpdateCluster(ctx, resourceDoc.InternalID, csCluster)
if err != nil {
logger.Error(err.Error())
arm.WriteCloudError(writer, CSErrorToCloudError(err, resourceID))
return
}
} else {
logger.Info(fmt.Sprintf("creating resource %s", resourceID))
csCluster, err = f.clusterServiceClient.PostCluster(ctx, csCluster)
if err != nil {
logger.Error(err.Error())
arm.WriteCloudError(writer, CSErrorToCloudError(err, resourceID))
return
}
resourceDoc.InternalID, err = ocm.NewInternalID(csCluster.HREF())
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
}
tracing.SetClusterAttributes(trace.SpanFromContext(ctx), csCluster)
operationDoc := database.NewOperationDocument(operationRequest, resourceDoc.ResourceID, resourceDoc.InternalID)
operationID, err := f.dbClient.CreateOperationDoc(ctx, operationDoc)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
pk := database.NewPartitionKey(resourceID.SubscriptionID)
err = f.ExposeOperation(writer, request, pk, operationID)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
// This is called directly when creating a resource, and indirectly from
// within a retry loop when updating a resource.
updateResourceMetadata := func(updateDoc *database.ResourceDocument) bool {
updateDoc.ActiveOperationID = operationID
updateDoc.ProvisioningState = operationDoc.Status
// Record managed identity type and any system-assigned identifiers.
// Omit the user-assigned identities map since that is reconstructed
// from Cluster Service data.
updateDoc.Identity = &arm.ManagedServiceIdentity{
PrincipalID: hcpCluster.Identity.PrincipalID,
TenantID: hcpCluster.Identity.TenantID,
Type: hcpCluster.Identity.Type,
}
// Record the latest system data values from ARM, if present.
if systemData != nil {
updateDoc.SystemData = systemData
}
// Here the difference between a nil map and an empty map is significant.
// If the Tags map is nil, that means it was omitted from the request body,
// so we leave any existing tags alone. If the Tags map is non-nil, even if
// empty, that means it was specified in the request body and should fully
// replace any existing tags.
if hcpCluster.Tags != nil {
updateDoc.Tags = hcpCluster.Tags
}
return true
}
if !updating {
updateResourceMetadata(resourceDoc)
err = f.dbClient.CreateResourceDoc(ctx, resourceDoc)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
logger.Info(fmt.Sprintf("document created for %s", resourceID))
} else {
updated, err := f.dbClient.UpdateResourceDoc(ctx, resourceID, updateResourceMetadata)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
if updated {
logger.Info(fmt.Sprintf("document updated for %s", resourceID))
}
// Get the updated resource document for the response.
resourceDoc, err = f.dbClient.GetResourceDoc(ctx, resourceID)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
}
responseBody, err := marshalCSCluster(csCluster, resourceDoc, versionedInterface)
if err != nil {
logger.Error(err.Error())
arm.WriteInternalServerError(writer)
return
}
_, err = arm.WriteJSONResponse(writer, successStatusCode, responseBody)
if err != nil {
logger.Error(err.Error())
}
}