From d264d4dd0aa13400d98ac15d7774ac659e40f15a Mon Sep 17 00:00:00 2001 From: Antony Natale Date: Tue, 21 May 2024 20:27:17 +0000 Subject: [PATCH 1/2] adds db calls for subscriptions --- frontend/database.go | 65 +++++++++++++++++++++++++++++++--- frontend/document.go | 32 +++++++++++++++++ frontend/frontend.go | 55 ++++++++++++++++++++++++++-- frontend/hcpclusterdocument.go | 16 --------- 4 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 frontend/document.go delete mode 100644 frontend/hcpclusterdocument.go diff --git a/frontend/database.go b/frontend/database.go index a1b99e0b2..d38cd4733 100644 --- a/frontend/database.go +++ b/frontend/database.go @@ -12,6 +12,9 @@ import ( const ( clustersContainer = "Clusters" + subsContainer = "Subscriptions" + billingContainer = "Billing" + asyncContainer = "AsyncOperations" ) // DBClient defines the needed values to perform CRUD operations against the async DB @@ -75,7 +78,7 @@ func (d *DBClient) DBConnectionTest(ctx context.Context) (string, error) { return result.DatabaseProperties.ID, nil } -// GetCluster retreives a cluster document from async DB using resource ID +// GetClusterDoc retreives a cluster document from async DB using resource ID func (d *DBClient) GetClusterDoc(ctx context.Context, resourceID string, partitionKey string) (*HCPOpenShiftClusterDocument, bool, error) { container, err := d.client.NewContainer(d.config.DBName, clustersContainer) if err != nil { @@ -109,10 +112,9 @@ func (d *DBClient) GetClusterDoc(ctx context.Context, resourceID string, partiti return doc, true, nil } return nil, false, nil - } -// SetCluster creates/updates a cluster document in the async DB during cluster creation/patching +// SetClusterDoc creates/updates a cluster document in the async DB during cluster creation/patching func (d *DBClient) SetClusterDoc(ctx context.Context, doc *HCPOpenShiftClusterDocument) error { data, err := json.Marshal(doc) if err != nil { @@ -132,7 +134,7 @@ func (d *DBClient) SetClusterDoc(ctx context.Context, doc *HCPOpenShiftClusterDo return nil } -// DeleteCluster removes a cluter document from the async DB using resource ID +// DeleteClusterDoc removes a cluter document from the async DB using resource ID func (d *DBClient) DeleteClusterDoc(ctx context.Context, resourceID string, partitionKey string) error { doc, found, err := d.GetClusterDoc(ctx, resourceID, partitionKey) if !found { @@ -153,3 +155,58 @@ func (d *DBClient) DeleteClusterDoc(ctx context.Context, resourceID string, part } return nil } + +// GetSubscriptionDoc retreives a subscription document from async DB using the subscription ID +func (d *DBClient) GetSubscriptionDoc(ctx context.Context, partitionKey string) (*SubscriptionDocument, bool, error) { + container, err := d.client.NewContainer(d.config.DBName, subsContainer) + if err != nil { + return nil, false, err + } + + query := "SELECT * FROM c WHERE c.partitionKey = @partitionKey" + opt := azcosmos.QueryOptions{ + PageSizeHint: 1, + QueryParameters: []azcosmos.QueryParameter{{Name: "@partitionKey", Value: partitionKey}}, + } + + pk := azcosmos.NewPartitionKeyString(partitionKey) + queryPager := container.NewQueryItemsPager(query, pk, &opt) + + var doc *SubscriptionDocument + for queryPager.More() { + queryResponse, err := queryPager.NextPage(ctx) + if err != nil { + return nil, false, err + } + + for _, item := range queryResponse.Items { + err = json.Unmarshal(item, &doc) + if err != nil { + return nil, false, err + } + } + } + if doc != nil { + return doc, true, nil + } + return nil, false, nil +} + +// SetClusterDoc creates/updates a subscription document in the async DB during cluster creation/patching +func (d *DBClient) SetSubscriptionDoc(ctx context.Context, doc *SubscriptionDocument) error { + data, err := json.Marshal(doc) + if err != nil { + return err + } + + container, err := d.client.NewContainer(d.config.DBName, subsContainer) + if err != nil { + return err + } + + _, err = container.UpsertItem(ctx, azcosmos.NewPartitionKeyString(doc.PartitionKey), data, nil) + if err != nil { + return err + } + return nil +} diff --git a/frontend/document.go b/frontend/document.go new file mode 100644 index 000000000..665d5b790 --- /dev/null +++ b/frontend/document.go @@ -0,0 +1,32 @@ +package main + +import "github.com/Azure/ARO-HCP/internal/api/arm" + +// HCPOpenShiftClusterDocument represents an HCP OpenShift cluster document. +type HCPOpenShiftClusterDocument struct { + ID string `json:"id,omitempty"` + Key string `json:"key,omitempty"` + PartitionKey string `json:"partitionKey,omitempty"` + ClusterID string `json:"clusterid,omitempty"` + + // Values provided by Cosmos after doc creation + ResourceID string `json:"_rid,omitempty"` + Self string `json:"_self,omitempty"` + ETag string `json:"_etag,omitempty"` + Attachments string `json:"_attachments,omitempty"` + Timestamp int `json:"_ts,omitempty"` +} + +// SubscriptionDocument represents an Azure Subscription document. +type SubscriptionDocument struct { + ID string `json:"id,omitempty"` + PartitionKey string `json:"partitionKey,omitempty"` + Subscription *arm.Subscription `json:"subscription,omitempty"` + + // Values provided by Cosmos after doc creation + ResourceID string `json:"_rid,omitempty"` + Self string `json:"_self,omitempty"` + ETag string `json:"_etag,omitempty"` + Attachments string `json:"_attachments,omitempty"` + Timestamp int `json:"_ts,omitempty"` +} diff --git a/frontend/frontend.go b/frontend/frontend.go index 2cfe19dfc..dc6f82eb1 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -90,7 +90,8 @@ func NewFrontend(logger *slog.Logger, listener net.Listener, emitter metrics.Emi mux.HandleFunc("/", f.NotFound) mux.HandleFunc(MuxPattern(http.MethodGet, "healthz", "ready"), f.HealthzReady) // TODO: determine where in the auth chain we should allow for this endpoint to be called by ARM - mux.HandleFunc(MuxPattern(http.MethodPut, PatternSubscriptions), f.ArmSubscriptionAction) + mux.HandleFunc(MuxPattern(http.MethodGet, PatternSubscriptions), f.ArmSubscriptionGet) + mux.HandleFunc(MuxPattern(http.MethodPut, PatternSubscriptions), f.ArmSubscriptionPut) // Expose Prometheus metrics endpoint mux.Handle(MuxPattern(http.MethodGet, "metrics"), promhttp.Handler()) @@ -413,7 +414,33 @@ func (f *Frontend) ArmResourceAction(writer http.ResponseWriter, request *http.R writer.WriteHeader(http.StatusOK) } -func (f *Frontend) ArmSubscriptionAction(writer http.ResponseWriter, request *http.Request) { +func (f *Frontend) ArmSubscriptionGet(writer http.ResponseWriter, request *http.Request) { + ctx := request.Context() + subId := request.PathValue(PathSegmentSubscriptionID) + + doc, found, err := f.dbClient.GetSubscriptionDoc(ctx, subId) + if err != nil { + f.logger.Error("failed to get document for subscription %s: %v", subId, err) + } + if !found { + f.logger.Error(fmt.Sprintf("document not found for subscription %s", subId)) + } + + resp, err := json.Marshal(&doc) + if err != nil { + f.logger.Error(err.Error()) + writer.WriteHeader(http.StatusInternalServerError) + return + } + _, err = writer.Write(resp) + if err != nil { + f.logger.Error(err.Error()) + } + + writer.WriteHeader(http.StatusOK) +} + +func (f *Frontend) ArmSubscriptionPut(writer http.ResponseWriter, request *http.Request) { ctx := request.Context() body, err := BodyFromContext(ctx) @@ -441,6 +468,28 @@ func (f *Frontend) ArmSubscriptionAction(writer http.ResponseWriter, request *ht "state": string(subscription.State), }) + var doc *SubscriptionDocument + doc, found, err := f.dbClient.GetSubscriptionDoc(ctx, subId) + if err != nil { + f.logger.Error("failed to fetch document for %s: %v", subId, err) + arm.WriteInternalServerError(writer) + return + } + if !found { + f.logger.Info(fmt.Sprintf("existing document not found for subscription - creating one for %s", subId)) + doc = &SubscriptionDocument{ + ID: uuid.New().String(), + PartitionKey: subId, + Subscription: &subscription, + } + } else { + doc.Subscription = &subscription + } + err = f.dbClient.SetSubscriptionDoc(ctx, doc) + if err != nil { + f.logger.Error("failed to create document for subscription %s: %v", subId, err) + } + resp, err := json.Marshal(subscription) if err != nil { f.logger.Error(err.Error()) @@ -451,6 +500,8 @@ func (f *Frontend) ArmSubscriptionAction(writer http.ResponseWriter, request *ht if err != nil { f.logger.Error(err.Error()) } + + writer.WriteHeader(http.StatusCreated) } func (f *Frontend) ArmDeploymentPreflight(writer http.ResponseWriter, request *http.Request) { diff --git a/frontend/hcpclusterdocument.go b/frontend/hcpclusterdocument.go deleted file mode 100644 index 22ce5b7ce..000000000 --- a/frontend/hcpclusterdocument.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -// HCPOpenShiftClusterDocument represents an HCP OpenShift cluster document. -type HCPOpenShiftClusterDocument struct { - ID string `json:"id,omitempty"` - Key string `json:"key,omitempty"` - PartitionKey string `json:"partitionKey,omitempty"` - ClusterID string `json:"clusterid,omitempty"` - - // Values provided by Cosmos after doc creation - ResourceID string `json:"_rid,omitempty"` - Self string `json:"_self,omitempty"` - ETag string `json:"_etag,omitempty"` - Attachments string `json:"_attachments,omitempty"` - Timestamp int `json:"_ts,omitempty"` -} From a6fc6c4fbd553cfda42f9c68f2aac741d81847e3 Mon Sep 17 00:00:00 2001 From: Antony Natale Date: Thu, 23 May 2024 19:07:29 +0000 Subject: [PATCH 2/2] removes bool in favor of custom error type --- frontend/database.go | 33 +++++++++--------- frontend/frontend.go | 79 +++++++++++++++++++++++++++----------------- 2 files changed, 64 insertions(+), 48 deletions(-) diff --git a/frontend/database.go b/frontend/database.go index d38cd4733..9e8431f92 100644 --- a/frontend/database.go +++ b/frontend/database.go @@ -3,7 +3,7 @@ package main import ( "context" "encoding/json" - "fmt" + "errors" "os" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" @@ -17,6 +17,8 @@ const ( asyncContainer = "AsyncOperations" ) +var ErrNotFound = errors.New("DocumentNotFound") + // DBClient defines the needed values to perform CRUD operations against the async DB type DBClient struct { client *azcosmos.Client @@ -79,10 +81,10 @@ func (d *DBClient) DBConnectionTest(ctx context.Context) (string, error) { } // GetClusterDoc retreives a cluster document from async DB using resource ID -func (d *DBClient) GetClusterDoc(ctx context.Context, resourceID string, partitionKey string) (*HCPOpenShiftClusterDocument, bool, error) { +func (d *DBClient) GetClusterDoc(ctx context.Context, resourceID string, partitionKey string) (*HCPOpenShiftClusterDocument, error) { container, err := d.client.NewContainer(d.config.DBName, clustersContainer) if err != nil { - return nil, false, err + return nil, err } query := "SELECT * FROM c WHERE c.key = @key" @@ -98,20 +100,20 @@ func (d *DBClient) GetClusterDoc(ctx context.Context, resourceID string, partiti for queryPager.More() { queryResponse, err := queryPager.NextPage(ctx) if err != nil { - return nil, false, err + return nil, err } for _, item := range queryResponse.Items { err = json.Unmarshal(item, &doc) if err != nil { - return nil, false, err + return nil, err } } } if doc != nil { - return doc, true, nil + return doc, nil } - return nil, false, nil + return nil, ErrNotFound } // SetClusterDoc creates/updates a cluster document in the async DB during cluster creation/patching @@ -136,10 +138,7 @@ func (d *DBClient) SetClusterDoc(ctx context.Context, doc *HCPOpenShiftClusterDo // DeleteClusterDoc removes a cluter document from the async DB using resource ID func (d *DBClient) DeleteClusterDoc(ctx context.Context, resourceID string, partitionKey string) error { - doc, found, err := d.GetClusterDoc(ctx, resourceID, partitionKey) - if !found { - return fmt.Errorf("document with key %s not found", partitionKey) - } + doc, err := d.GetClusterDoc(ctx, resourceID, partitionKey) if err != nil { return err } @@ -157,10 +156,10 @@ func (d *DBClient) DeleteClusterDoc(ctx context.Context, resourceID string, part } // GetSubscriptionDoc retreives a subscription document from async DB using the subscription ID -func (d *DBClient) GetSubscriptionDoc(ctx context.Context, partitionKey string) (*SubscriptionDocument, bool, error) { +func (d *DBClient) GetSubscriptionDoc(ctx context.Context, partitionKey string) (*SubscriptionDocument, error) { container, err := d.client.NewContainer(d.config.DBName, subsContainer) if err != nil { - return nil, false, err + return nil, err } query := "SELECT * FROM c WHERE c.partitionKey = @partitionKey" @@ -176,20 +175,20 @@ func (d *DBClient) GetSubscriptionDoc(ctx context.Context, partitionKey string) for queryPager.More() { queryResponse, err := queryPager.NextPage(ctx) if err != nil { - return nil, false, err + return nil, err } for _, item := range queryResponse.Items { err = json.Unmarshal(item, &doc) if err != nil { - return nil, false, err + return nil, err } } } if doc != nil { - return doc, true, nil + return doc, nil } - return nil, false, nil + return nil, ErrNotFound } // SetClusterDoc creates/updates a subscription document in the async DB during cluster creation/patching diff --git a/frontend/frontend.go b/frontend/frontend.go index dc6f82eb1..11a28b484 100644 --- a/frontend/frontend.go +++ b/frontend/frontend.go @@ -6,6 +6,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "net" @@ -314,21 +315,23 @@ func (f *Frontend) ArmResourceCreateOrUpdate(writer http.ResponseWriter, request parsed, _ := azure.ParseResourceID(resourceID) var doc *HCPOpenShiftClusterDocument - doc, found, err := f.dbClient.GetClusterDoc(ctx, resourceID, parsed.SubscriptionID) + doc, err = f.dbClient.GetClusterDoc(ctx, resourceID, parsed.SubscriptionID) if err != nil { - f.logger.Error("failed to fetch document for %s: %v", resourceID, err) - arm.WriteInternalServerError(writer) - return - } - if !found { - f.logger.Info(fmt.Sprintf("existing document not found for cluster - creating one for %s", resourceID)) - doc = &HCPOpenShiftClusterDocument{ - ID: uuid.New().String(), - Key: resourceID, - ClusterID: NewUID(), - PartitionKey: parsed.SubscriptionID, + if errors.Is(err, ErrNotFound) { + f.logger.Info(fmt.Sprintf("existing document not found for cluster - creating one for %s", resourceID)) + doc = &HCPOpenShiftClusterDocument{ + ID: uuid.New().String(), + Key: resourceID, + ClusterID: NewUID(), + PartitionKey: parsed.SubscriptionID, + } + } else { + f.logger.Error("failed to fetch document for %s: %v", resourceID, err) + arm.WriteInternalServerError(writer) + return } } + err = f.dbClient.SetClusterDoc(ctx, doc) if err != nil { f.logger.Error("failed to create document for resource %s: %v", resourceID, err) @@ -390,9 +393,15 @@ func (f *Frontend) ArmResourceDelete(writer http.ResponseWriter, request *http.R parsed, _ := azure.ParseResourceID(resourceID) err = f.dbClient.DeleteClusterDoc(ctx, resourceID, parsed.SubscriptionID) if err != nil { - f.logger.Error(err.Error()) - arm.WriteInternalServerError(writer) - return + if errors.Is(err, ErrNotFound) { + f.logger.Info(fmt.Sprintf("cluster document cannot be deleted -- document not found for %s", resourceID)) + writer.WriteHeader(http.StatusNoContent) + return + } else { + f.logger.Error(err.Error()) + arm.WriteInternalServerError(writer) + return + } } f.logger.Info(fmt.Sprintf("document deleted for resource %s", resourceID)) @@ -418,12 +427,17 @@ func (f *Frontend) ArmSubscriptionGet(writer http.ResponseWriter, request *http. ctx := request.Context() subId := request.PathValue(PathSegmentSubscriptionID) - doc, found, err := f.dbClient.GetSubscriptionDoc(ctx, subId) + doc, err := f.dbClient.GetSubscriptionDoc(ctx, subId) if err != nil { - f.logger.Error("failed to get document for subscription %s: %v", subId, err) - } - if !found { - f.logger.Error(fmt.Sprintf("document not found for subscription %s", subId)) + if errors.Is(err, ErrNotFound) { + f.logger.Error(fmt.Sprintf("document not found for subscription %s", subId)) + writer.WriteHeader(http.StatusNotFound) + return + } else { + f.logger.Error(err.Error()) + writer.WriteHeader(http.StatusInternalServerError) + return + } } resp, err := json.Marshal(&doc) @@ -469,22 +483,25 @@ func (f *Frontend) ArmSubscriptionPut(writer http.ResponseWriter, request *http. }) var doc *SubscriptionDocument - doc, found, err := f.dbClient.GetSubscriptionDoc(ctx, subId) + doc, err = f.dbClient.GetSubscriptionDoc(ctx, subId) if err != nil { - f.logger.Error("failed to fetch document for %s: %v", subId, err) - arm.WriteInternalServerError(writer) - return - } - if !found { - f.logger.Info(fmt.Sprintf("existing document not found for subscription - creating one for %s", subId)) - doc = &SubscriptionDocument{ - ID: uuid.New().String(), - PartitionKey: subId, - Subscription: &subscription, + if errors.Is(err, ErrNotFound) { + f.logger.Info(fmt.Sprintf("existing document not found for subscription - creating one for %s", subId)) + doc = &SubscriptionDocument{ + ID: uuid.New().String(), + PartitionKey: subId, + Subscription: &subscription, + } + } else { + f.logger.Error("failed to fetch document for %s: %v", subId, err) + arm.WriteInternalServerError(writer) + return } } else { + f.logger.Info(fmt.Sprintf("existing document found for subscription - will update document for subscription %s", subId)) doc.Subscription = &subscription } + err = f.dbClient.SetSubscriptionDoc(ctx, doc) if err != nil { f.logger.Error("failed to create document for subscription %s: %v", subId, err)