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
}