internal/database/document.go (107 lines of code) (raw):

// Copyright 2025 Microsoft Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package database import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/google/uuid" "github.com/Azure/ARO-HCP/internal/api" "github.com/Azure/ARO-HCP/internal/api/arm" "github.com/Azure/ARO-HCP/internal/ocm" ) // baseDocument includes fields common to all container items. type baseDocument struct { ID string `json:"id,omitempty"` // https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/time-to-live TimeToLive int `json:"ttl,omitempty"` // System-defined properties generated by Cosmos DB CosmosResourceID string `json:"_rid,omitempty"` CosmosSelf string `json:"_self,omitempty"` CosmosETag azcore.ETag `json:"_etag,omitempty"` CosmosAttachments string `json:"_attachments,omitempty"` CosmosTimestamp int `json:"_ts,omitempty"` } // newBaseDocument returns a baseDocument with a unique ID. func newBaseDocument() baseDocument { return baseDocument{ID: uuid.New().String()} } // DocumentProperties is an interface for types that can serve as // typedDocument.Properties. type DocumentProperties interface { GetValidTypes() []string } // ResourceDocument captures the mapping of an Azure resource ID // to an internal resource ID (the OCM API path), as well as any // ARM-specific metadata for the resource. type ResourceDocument struct { ResourceID *azcorearm.ResourceID `json:"resourceId,omitempty"` InternalID ocm.InternalID `json:"internalId,omitempty"` ActiveOperationID string `json:"activeOperationId,omitempty"` ProvisioningState arm.ProvisioningState `json:"provisioningState,omitempty"` Identity *arm.ManagedServiceIdentity `json:"identity,omitempty"` SystemData *arm.SystemData `json:"systemData,omitempty"` Tags map[string]string `json:"tags,omitempty"` } func NewResourceDocument(resourceID *azcorearm.ResourceID) *ResourceDocument { return &ResourceDocument{ ResourceID: resourceID, } } // GetValidTypes returns the valid resource types for a ResourceDocument. func (doc ResourceDocument) GetValidTypes() []string { return []string{ api.ClusterResourceType.String(), api.NodePoolResourceType.String(), } } type OperationRequest string const ( OperationRequestCreate OperationRequest = "Create" OperationRequestUpdate OperationRequest = "Update" OperationRequestDelete OperationRequest = "Delete" // These are for POST actions on resources. OperationRequestRequestCredential OperationRequest = "RequestCredential" OperationRequestRevokeCredentials OperationRequest = "RevokeCredentials" ) // OperationResourceType is an artificial resource type for OperationDocuments // in Cosmos DB. It omits the location segment from actual operation endpoints. var OperationResourceType = azcorearm.NewResourceType(api.ProviderNamespace, api.OperationStatusResourceTypeName) // OperationDocument tracks an asynchronous operation. type OperationDocument struct { // TenantID is the tenant ID of the client that requested the operation TenantID string `json:"tenantId,omitempty"` // ClientID is the object ID of the client that requested the operation ClientID string `json:"clientId,omitempty"` // Request is the type of asynchronous operation requested Request OperationRequest `json:"request,omitempty"` // ExternalID is the Azure resource ID of the cluster or node pool ExternalID *azcorearm.ResourceID `json:"externalId,omitempty"` // InternalID is the Cluster Service resource identifier in the form of a URL path InternalID ocm.InternalID `json:"internalId,omitempty"` // OperationID is the Azure resource ID of the operation status (may be nil if the // operation was implicit, such as deleting a child resource along with the parent) OperationID *azcorearm.ResourceID `json:"operationId,omitempty"` // NotificationURI is provided by the Azure-AsyncNotificationUri header if the // Async Operation Callbacks ARM feature is enabled NotificationURI string `json:"notificationUri,omitempty"` // StartTime marks the start of the operation StartTime time.Time `json:"startTime,omitempty"` // LastTransitionTime marks the most recent state change LastTransitionTime time.Time `json:"lastTransitionTime,omitempty"` // Status is the current operation status, using the same set of values // as the resource's provisioning state Status arm.ProvisioningState `json:"status,omitempty"` // Error is an OData error, present when Status is "Failed" or "Canceled" Error *arm.CloudErrorBody `json:"error,omitempty"` } func NewOperationDocument(request OperationRequest, externalID *azcorearm.ResourceID, internalID ocm.InternalID) *OperationDocument { now := time.Now().UTC() doc := &OperationDocument{ Request: request, ExternalID: externalID, InternalID: internalID, StartTime: now, LastTransitionTime: now, Status: arm.ProvisioningStateAccepted, } // When deleting, set Status directly to ProvisioningStateDeleting // so any further deletion requests are rejected with 409 Conflict. if request == OperationRequestDelete { doc.Status = arm.ProvisioningStateDeleting } return doc } // GetValidTypes returns the valid resource types for an OperationDocument. func (doc OperationDocument) GetValidTypes() []string { return []string{OperationResourceType.String()} } // ToStatus converts an OperationDocument to the ARM operation status format. func (doc *OperationDocument) ToStatus() *arm.Operation { operation := &arm.Operation{ ID: doc.OperationID, Name: doc.OperationID.Name, Status: doc.Status, StartTime: &doc.StartTime, Error: doc.Error, } if doc.Status.IsTerminal() { operation.EndTime = &doc.LastTransitionTime } return operation } // UpdateStatus conditionally updates the document if the status given differs // from the status already present. If so, it sets the Status and Error fields // to the values given, updates the LastTransitionTime, and returns true. This // is intended to be used with DBClient.UpdateOperationDoc. func (doc *OperationDocument) UpdateStatus(status arm.ProvisioningState, err *arm.CloudErrorBody) bool { if doc.Status != status { doc.LastTransitionTime = time.Now().UTC() doc.Status = status doc.Error = err return true } return false }