зеркало из https://github.com/Azure/ARO-RP.git
Billing RP Operations (#238)
* bootstrap billing RP operation * add Uts * Add tenantid location / move update of deltionTs after async / update UTs * add az account set command into e2e helper * refactor based on review * refactor and move operations in the backend * Update trigger * update patch logic and Uts * update UTs * Add log line and lint UTs * applying review. and making the billing flow simplier * Refactor Patch operation * make generate * removing the init part of lastBillingTime in the creation trigger
This commit is contained in:
Родитель
44e43af806
Коммит
8a51a88b57
|
@ -46,6 +46,27 @@
|
|||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), parameters('databaseName'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "Billing",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
],
|
||||
"kind": "Hash"
|
||||
}
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"name": "[concat(parameters('databaseAccountName'), '/', parameters('databaseName'), '/Billing')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"location": "[resourceGroup().location]",
|
||||
"apiVersion": "2019-08-01",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), parameters('databaseName'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"resource": {
|
||||
|
|
|
@ -491,6 +491,28 @@
|
|||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"resource": {
|
||||
"id": "Billing",
|
||||
"partitionKey": {
|
||||
"paths": [
|
||||
"/id"
|
||||
],
|
||||
"kind": "Hash"
|
||||
}
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"name": "[concat(parameters('databaseAccountName'), '/', 'ARO', '/Billing')]",
|
||||
"type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers",
|
||||
"location": "[resourceGroup().location]",
|
||||
"apiVersion": "2019-08-01",
|
||||
"dependsOn": [
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), 'ARO')]",
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"resource": {
|
||||
|
|
|
@ -134,6 +134,7 @@ echo "######################################"
|
|||
echo "######## Current settings : ##########"
|
||||
echo
|
||||
echo "LOCATION=$LOCATION"
|
||||
echo "AZURE_SUBSCRIPTION_ID=$AZURE_SUBSCRIPTION_ID"
|
||||
echo
|
||||
echo "COSMOSDB_ACCOUNT=$COSMOSDB_ACCOUNT"
|
||||
echo "DATABASE_NAME=$DATABASE_NAME"
|
||||
|
@ -151,3 +152,6 @@ echo "######################################"
|
|||
[ "$PROXY_HOSTNAME" ] || ( echo ">> PROXY_HOSTNAME is not set please validate your ./secrets/env"; exit 128 )
|
||||
[ "$COSMOSDB_ACCOUNT" ] || ( echo ">> COSMOSDB_ACCOUNT is not set please validate your ./secrets/env"; exit 128 )
|
||||
[ "$DATABASE_NAME" ] || ( echo ">> DATABASE_NAME is not set please validate your ./secrets/env"; exit 128 )
|
||||
[ "$AZURE_SUBSCRIPTION_ID" ] || ( echo ">> AZURE_SUBSCRIPTION_ID is not set please validate your ./secrets/env"; exit 128 )
|
||||
|
||||
az account set -s $AZURE_SUBSCRIPTION_ID >/dev/null
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package api
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
// Billing represents a Billing entry
|
||||
type Billing struct {
|
||||
MissingFields
|
||||
|
||||
CreationTime int `json:"creationTime,omitempty"`
|
||||
DeletionTime int `json:"deletionTime,omitempty"`
|
||||
LastBillingTime int `json:"lastBillingTime,omitempty"`
|
||||
|
||||
Location string `json:"location,omitempty"`
|
||||
TenantID string `json:"tenantID,omitempty"`
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package api
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
// BillingDocuments represents billing documents.
|
||||
// pkg/database/cosmosdb requires its definition.
|
||||
type BillingDocuments struct {
|
||||
Count int `json:"_count,omitempty"`
|
||||
ResourceID string `json:"_rid,omitempty"`
|
||||
BillingDocuments []*BillingDocument `json:"Documents,omitempty"`
|
||||
}
|
||||
|
||||
// BillingDocument represents a billing document.
|
||||
// pkg/database/cosmosdb requires its definition.
|
||||
type BillingDocument struct {
|
||||
MissingFields
|
||||
|
||||
ID string `json:"id,omitempty"`
|
||||
ResourceID string `json:"_rid,omitempty"`
|
||||
Timestamp int `json:"_ts,omitempty"`
|
||||
Self string `json:"_self,omitempty"`
|
||||
ETag string `json:"_etag,omitempty"`
|
||||
Attachments string `json:"_attachments,omitempty"`
|
||||
LSN int `json:"_lsn,omitempty"`
|
||||
Metadata map[string]interface{} `json:"_metadata,omitempty"`
|
||||
|
||||
Billing *Billing `json:"billing,omitempty"`
|
||||
|
||||
Key string `json:"key,omitempty"`
|
||||
ClusterResourceGroupIDKey string `json:"clusterResourceGroupIDKey,omitempty"`
|
||||
}
|
|
@ -92,7 +92,7 @@ func (ocb *openShiftClusterBackend) handle(ctx context.Context, log *logrus.Entr
|
|||
stop := ocb.heartbeat(ctx, cancel, log, doc)
|
||||
defer stop()
|
||||
|
||||
m, err := openshiftcluster.NewManager(log, ocb.env, ocb.db.OpenShiftClusters, doc)
|
||||
m, err := openshiftcluster.NewManager(log, ocb.env, ocb.db.OpenShiftClusters, ocb.db.Billing, doc)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return ocb.endLease(ctx, stop, doc, api.ProvisioningStateFailed, err)
|
||||
|
|
|
@ -231,7 +231,7 @@ func (m *Manager) Create(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
i, err := install.NewInstaller(ctx, m.log, m.env, m.db, m.doc)
|
||||
i, err := install.NewInstaller(ctx, m.log, m.env, m.db, m.billing, m.doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -91,5 +91,13 @@ func (m *Manager) Delete(ctx context.Context) error {
|
|||
detailedErr.StatusCode == http.StatusForbidden {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.log.Printf("updating billing record with deletion time")
|
||||
_, err = m.billing.MarkForDeletion(ctx, m.doc.ID)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ type Manager struct {
|
|||
log *logrus.Entry
|
||||
env env.Interface
|
||||
db database.OpenShiftClusters
|
||||
billing database.Billing
|
||||
fpAuthorizer autorest.Authorizer
|
||||
|
||||
groups resources.GroupsClient
|
||||
|
@ -34,7 +35,7 @@ type Manager struct {
|
|||
doc *api.OpenShiftClusterDocument
|
||||
}
|
||||
|
||||
func NewManager(log *logrus.Entry, env env.Interface, db database.OpenShiftClusters, doc *api.OpenShiftClusterDocument) (*Manager, error) {
|
||||
func NewManager(log *logrus.Entry, env env.Interface, db database.OpenShiftClusters, billing database.Billing, doc *api.OpenShiftClusterDocument) (*Manager, error) {
|
||||
r, err := azure.ParseResourceID(doc.OpenShiftCluster.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -59,6 +60,7 @@ func NewManager(log *logrus.Entry, env env.Interface, db database.OpenShiftClust
|
|||
log: log,
|
||||
env: env,
|
||||
db: db,
|
||||
billing: billing,
|
||||
fpAuthorizer: fpAuthorizer,
|
||||
|
||||
groups: resources.NewGroupsClient(r.SubscriptionID, fpAuthorizer),
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package database
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
|
||||
)
|
||||
|
||||
type billing struct {
|
||||
c cosmosdb.BillingDocumentClient
|
||||
uuid string
|
||||
}
|
||||
|
||||
// Billing is the database interface for BillingDocuments
|
||||
type Billing interface {
|
||||
Create(context.Context, *api.BillingDocument) (*api.BillingDocument, error)
|
||||
Get(context.Context, string) (*api.BillingDocument, error)
|
||||
MarkForDeletion(context.Context, string) (*api.BillingDocument, error)
|
||||
}
|
||||
|
||||
// NewBilling returns a new Billing
|
||||
func NewBilling(ctx context.Context, uuid string, dbc cosmosdb.DatabaseClient, dbid, collid string) (Billing, error) {
|
||||
collc := cosmosdb.NewCollectionClient(dbc, dbid)
|
||||
|
||||
triggers := []*cosmosdb.Trigger{
|
||||
{
|
||||
ID: "setCreationBillingTimeStamp",
|
||||
TriggerOperation: cosmosdb.TriggerOperationCreate,
|
||||
TriggerType: cosmosdb.TriggerTypePre,
|
||||
Body: `function trigger() {
|
||||
var request = getContext().getRequest();
|
||||
var body = request.getBody();
|
||||
var date = new Date();
|
||||
var now = Math.floor(date.getTime() / 1000);
|
||||
var billingBody = body["billing"];
|
||||
if (!billingBody["creationTime"]) {
|
||||
billingBody["creationTime"] = now;
|
||||
}
|
||||
request.setBody(body);
|
||||
}`,
|
||||
},
|
||||
{
|
||||
ID: "setDeletionBillingTimeStamp",
|
||||
TriggerOperation: cosmosdb.TriggerOperationReplace,
|
||||
TriggerType: cosmosdb.TriggerTypePre,
|
||||
Body: `function trigger() {
|
||||
var request = getContext().getRequest();
|
||||
var body = request.getBody();
|
||||
var date = new Date();
|
||||
var now = Math.floor(date.getTime() / 1000);
|
||||
var billingBody = body["billing"];
|
||||
if (!billingBody["deletionTime"]) {
|
||||
billingBody["deletionTime"] = now;
|
||||
}
|
||||
request.setBody(body);
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
triggerc := cosmosdb.NewTriggerClient(collc, collid)
|
||||
for _, trigger := range triggers {
|
||||
_, err := triggerc.Create(ctx, trigger)
|
||||
if err != nil && !cosmosdb.IsErrorStatusCode(err, http.StatusConflict) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &billing{
|
||||
c: cosmosdb.NewBillingDocumentClient(collc, collid),
|
||||
uuid: uuid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Creating Billing Document
|
||||
func (c *billing) Create(ctx context.Context, doc *api.BillingDocument) (*api.BillingDocument, error) {
|
||||
if doc.ID != strings.ToLower(doc.ID) {
|
||||
return nil, fmt.Errorf("id %q is not lower case", doc.ID)
|
||||
}
|
||||
|
||||
return c.c.Create(ctx, doc.ID, doc, &cosmosdb.Options{PreTriggers: []string{"setCreationBillingTimeStamp"}})
|
||||
}
|
||||
|
||||
func (c *billing) Get(ctx context.Context, id string) (*api.BillingDocument, error) {
|
||||
if id != strings.ToLower(id) {
|
||||
return nil, fmt.Errorf("id %q is not lower case", id)
|
||||
}
|
||||
|
||||
return c.c.Get(ctx, id, id, nil)
|
||||
}
|
||||
|
||||
func (c *billing) patch(ctx context.Context, id string, f func(*api.BillingDocument) error, options *cosmosdb.Options) (*api.BillingDocument, error) {
|
||||
var doc *api.BillingDocument
|
||||
|
||||
err := cosmosdb.RetryOnPreconditionFailed(func() (err error) {
|
||||
doc, err = c.Get(ctx, id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = f(doc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
doc, err = c.c.Replace(ctx, doc.ID, doc, options)
|
||||
return
|
||||
})
|
||||
|
||||
return doc, err
|
||||
}
|
||||
|
||||
// MarkForDeletion update the deletion timestamp field in the document
|
||||
func (c *billing) MarkForDeletion(ctx context.Context, id string) (*api.BillingDocument, error) {
|
||||
return c.patch(ctx, id, func(billingdoc *api.BillingDocument) error {
|
||||
return nil
|
||||
}, &cosmosdb.Options{PreTriggers: []string{"setDeletionBillingTimeStamp"}})
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
//go:generate go run ../../../vendor/github.com/jim-minter/go-cosmosdb/cmd/gencosmosdb github.com/Azure/ARO-RP/pkg/api,AsyncOperationDocument github.com/Azure/ARO-RP/pkg/api,MonitorDocument github.com/Azure/ARO-RP/pkg/api,OpenShiftClusterDocument github.com/Azure/ARO-RP/pkg/api,SubscriptionDocument
|
||||
//go:generate go run ../../../vendor/github.com/jim-minter/go-cosmosdb/cmd/gencosmosdb github.com/Azure/ARO-RP/pkg/api,AsyncOperationDocument github.com/Azure/ARO-RP/pkg/api,BillingDocument github.com/Azure/ARO-RP/pkg/api,MonitorDocument github.com/Azure/ARO-RP/pkg/api,OpenShiftClusterDocument github.com/Azure/ARO-RP/pkg/api,SubscriptionDocument
|
||||
//go:generate go run ../../../vendor/github.com/golang/mock/mockgen -destination=../../util/mocks/database/$GOPACKAGE/$GOPACKAGE.go github.com/Azure/ARO-RP/pkg/database/$GOPACKAGE OpenShiftClusterDocumentIterator
|
||||
//go:generate go run ../../../vendor/golang.org/x/tools/cmd/goimports -local=github.com/Azure/ARO-RP -e -w ../../util/mocks/database/$GOPACKAGE/$GOPACKAGE.go
|
||||
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
// Code generated by github.com/jim-minter/go-cosmosdb, DO NOT EDIT.
|
||||
|
||||
package cosmosdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
pkg "github.com/Azure/ARO-RP/pkg/api"
|
||||
)
|
||||
|
||||
type billingDocumentClient struct {
|
||||
*databaseClient
|
||||
path string
|
||||
}
|
||||
|
||||
// BillingDocumentClient is a billingDocument client
|
||||
type BillingDocumentClient interface {
|
||||
Create(context.Context, string, *pkg.BillingDocument, *Options) (*pkg.BillingDocument, error)
|
||||
List(*Options) BillingDocumentRawIterator
|
||||
ListAll(context.Context, *Options) (*pkg.BillingDocuments, error)
|
||||
Get(context.Context, string, string, *Options) (*pkg.BillingDocument, error)
|
||||
Replace(context.Context, string, *pkg.BillingDocument, *Options) (*pkg.BillingDocument, error)
|
||||
Delete(context.Context, string, *pkg.BillingDocument, *Options) error
|
||||
Query(string, *Query, *Options) BillingDocumentRawIterator
|
||||
QueryAll(context.Context, string, *Query, *Options) (*pkg.BillingDocuments, error)
|
||||
ChangeFeed(*Options) BillingDocumentIterator
|
||||
}
|
||||
|
||||
type billingDocumentChangeFeedIterator struct {
|
||||
*billingDocumentClient
|
||||
continuation string
|
||||
options *Options
|
||||
}
|
||||
|
||||
type billingDocumentListIterator struct {
|
||||
*billingDocumentClient
|
||||
continuation string
|
||||
done bool
|
||||
options *Options
|
||||
}
|
||||
|
||||
type billingDocumentQueryIterator struct {
|
||||
*billingDocumentClient
|
||||
partitionkey string
|
||||
query *Query
|
||||
continuation string
|
||||
done bool
|
||||
options *Options
|
||||
}
|
||||
|
||||
// BillingDocumentIterator is a billingDocument iterator
|
||||
type BillingDocumentIterator interface {
|
||||
Next(context.Context) (*pkg.BillingDocuments, error)
|
||||
}
|
||||
|
||||
// BillingDocumentRawIterator is a billingDocument raw iterator
|
||||
type BillingDocumentRawIterator interface {
|
||||
BillingDocumentIterator
|
||||
NextRaw(context.Context, interface{}) error
|
||||
}
|
||||
|
||||
// NewBillingDocumentClient returns a new billingDocument client
|
||||
func NewBillingDocumentClient(collc CollectionClient, collid string) BillingDocumentClient {
|
||||
return &billingDocumentClient{
|
||||
databaseClient: collc.(*collectionClient).databaseClient,
|
||||
path: collc.(*collectionClient).path + "/colls/" + collid,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) all(ctx context.Context, i BillingDocumentIterator) (*pkg.BillingDocuments, error) {
|
||||
allbillingDocuments := &pkg.BillingDocuments{}
|
||||
|
||||
for {
|
||||
billingDocuments, err := i.Next(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if billingDocuments == nil {
|
||||
break
|
||||
}
|
||||
|
||||
allbillingDocuments.Count += billingDocuments.Count
|
||||
allbillingDocuments.ResourceID = billingDocuments.ResourceID
|
||||
allbillingDocuments.BillingDocuments = append(allbillingDocuments.BillingDocuments, billingDocuments.BillingDocuments...)
|
||||
}
|
||||
|
||||
return allbillingDocuments, nil
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) Create(ctx context.Context, partitionkey string, newbillingDocument *pkg.BillingDocument, options *Options) (billingDocument *pkg.BillingDocument, err error) {
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Ms-Documentdb-Partitionkey", `["`+partitionkey+`"]`)
|
||||
|
||||
if options == nil {
|
||||
options = &Options{}
|
||||
}
|
||||
options.NoETag = true
|
||||
|
||||
err = c.setOptions(options, newbillingDocument, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.do(ctx, http.MethodPost, c.path+"/docs", "docs", c.path, http.StatusCreated, &newbillingDocument, &billingDocument, headers)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) List(options *Options) BillingDocumentRawIterator {
|
||||
return &billingDocumentListIterator{billingDocumentClient: c, options: options}
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) ListAll(ctx context.Context, options *Options) (*pkg.BillingDocuments, error) {
|
||||
return c.all(ctx, c.List(options))
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) Get(ctx context.Context, partitionkey, billingDocumentid string, options *Options) (billingDocument *pkg.BillingDocument, err error) {
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Ms-Documentdb-Partitionkey", `["`+partitionkey+`"]`)
|
||||
|
||||
err = c.setOptions(options, nil, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.do(ctx, http.MethodGet, c.path+"/docs/"+billingDocumentid, "docs", c.path+"/docs/"+billingDocumentid, http.StatusOK, nil, &billingDocument, headers)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) Replace(ctx context.Context, partitionkey string, newbillingDocument *pkg.BillingDocument, options *Options) (billingDocument *pkg.BillingDocument, err error) {
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Ms-Documentdb-Partitionkey", `["`+partitionkey+`"]`)
|
||||
|
||||
err = c.setOptions(options, newbillingDocument, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.do(ctx, http.MethodPut, c.path+"/docs/"+newbillingDocument.ID, "docs", c.path+"/docs/"+newbillingDocument.ID, http.StatusOK, &newbillingDocument, &billingDocument, headers)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) Delete(ctx context.Context, partitionkey string, billingDocument *pkg.BillingDocument, options *Options) (err error) {
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Ms-Documentdb-Partitionkey", `["`+partitionkey+`"]`)
|
||||
|
||||
err = c.setOptions(options, billingDocument, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = c.do(ctx, http.MethodDelete, c.path+"/docs/"+billingDocument.ID, "docs", c.path+"/docs/"+billingDocument.ID, http.StatusNoContent, nil, nil, headers)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) Query(partitionkey string, query *Query, options *Options) BillingDocumentRawIterator {
|
||||
return &billingDocumentQueryIterator{billingDocumentClient: c, partitionkey: partitionkey, query: query, options: options}
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) QueryAll(ctx context.Context, partitionkey string, query *Query, options *Options) (*pkg.BillingDocuments, error) {
|
||||
return c.all(ctx, c.Query(partitionkey, query, options))
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) ChangeFeed(options *Options) BillingDocumentIterator {
|
||||
return &billingDocumentChangeFeedIterator{billingDocumentClient: c}
|
||||
}
|
||||
|
||||
func (c *billingDocumentClient) setOptions(options *Options, billingDocument *pkg.BillingDocument, headers http.Header) error {
|
||||
if options == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if billingDocument != nil && !options.NoETag {
|
||||
if billingDocument.ETag == "" {
|
||||
return ErrETagRequired
|
||||
}
|
||||
headers.Set("If-Match", billingDocument.ETag)
|
||||
}
|
||||
if len(options.PreTriggers) > 0 {
|
||||
headers.Set("X-Ms-Documentdb-Pre-Trigger-Include", strings.Join(options.PreTriggers, ","))
|
||||
}
|
||||
if len(options.PostTriggers) > 0 {
|
||||
headers.Set("X-Ms-Documentdb-Post-Trigger-Include", strings.Join(options.PostTriggers, ","))
|
||||
}
|
||||
if len(options.PartitionKeyRangeID) > 0 {
|
||||
headers.Set("X-Ms-Documentdb-PartitionKeyRangeID", options.PartitionKeyRangeID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *billingDocumentChangeFeedIterator) Next(ctx context.Context) (billingDocuments *pkg.BillingDocuments, err error) {
|
||||
headers := http.Header{}
|
||||
headers.Set("A-IM", "Incremental feed")
|
||||
|
||||
headers.Set("X-Ms-Max-Item-Count", "-1")
|
||||
if i.continuation != "" {
|
||||
headers.Set("If-None-Match", i.continuation)
|
||||
}
|
||||
|
||||
err = i.setOptions(i.options, nil, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = i.do(ctx, http.MethodGet, i.path+"/docs", "docs", i.path, http.StatusOK, nil, &billingDocuments, headers)
|
||||
if IsErrorStatusCode(err, http.StatusNotModified) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.continuation = headers.Get("Etag")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *billingDocumentListIterator) Next(ctx context.Context) (billingDocuments *pkg.BillingDocuments, err error) {
|
||||
err = i.NextRaw(ctx, &billingDocuments)
|
||||
return
|
||||
}
|
||||
|
||||
func (i *billingDocumentListIterator) NextRaw(ctx context.Context, raw interface{}) (err error) {
|
||||
if i.done {
|
||||
return
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Ms-Max-Item-Count", "-1")
|
||||
if i.continuation != "" {
|
||||
headers.Set("X-Ms-Continuation", i.continuation)
|
||||
}
|
||||
|
||||
err = i.setOptions(i.options, nil, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = i.do(ctx, http.MethodGet, i.path+"/docs", "docs", i.path, http.StatusOK, nil, &raw, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.continuation = headers.Get("X-Ms-Continuation")
|
||||
i.done = i.continuation == ""
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (i *billingDocumentQueryIterator) Next(ctx context.Context) (billingDocuments *pkg.BillingDocuments, err error) {
|
||||
err = i.NextRaw(ctx, &billingDocuments)
|
||||
return
|
||||
}
|
||||
|
||||
func (i *billingDocumentQueryIterator) NextRaw(ctx context.Context, raw interface{}) (err error) {
|
||||
if i.done {
|
||||
return
|
||||
}
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("X-Ms-Max-Item-Count", "-1")
|
||||
headers.Set("X-Ms-Documentdb-Isquery", "True")
|
||||
headers.Set("Content-Type", "application/query+json")
|
||||
if i.partitionkey != "" {
|
||||
headers.Set("X-Ms-Documentdb-Partitionkey", `["`+i.partitionkey+`"]`)
|
||||
} else {
|
||||
headers.Set("X-Ms-Documentdb-Query-Enablecrosspartition", "True")
|
||||
}
|
||||
if i.continuation != "" {
|
||||
headers.Set("X-Ms-Continuation", i.continuation)
|
||||
}
|
||||
|
||||
err = i.setOptions(i.options, nil, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = i.do(ctx, http.MethodPost, i.path+"/docs", "docs", i.path, http.StatusOK, &i.query, &raw, headers)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
i.continuation = headers.Get("X-Ms-Continuation")
|
||||
i.done = i.continuation == ""
|
||||
|
||||
return
|
||||
}
|
|
@ -27,6 +27,7 @@ type Database struct {
|
|||
m metrics.Interface
|
||||
|
||||
AsyncOperations AsyncOperations
|
||||
Billing Billing
|
||||
Monitors Monitors
|
||||
OpenShiftClusters OpenShiftClusters
|
||||
Subscriptions Subscriptions
|
||||
|
@ -62,6 +63,11 @@ func NewDatabase(ctx context.Context, log *logrus.Entry, env env.Interface, m me
|
|||
return nil, err
|
||||
}
|
||||
|
||||
db.Billing, err = NewBilling(ctx, uuid, dbc, env.DatabaseName(), "Billing")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.Monitors, err = NewMonitors(ctx, uuid, dbc, env.DatabaseName(), "Monitors")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -3,5 +3,5 @@ package database
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
//go:generate go run ../../vendor/github.com/golang/mock/mockgen -destination=../util/mocks/$GOPACKAGE/$GOPACKAGE.go github.com/Azure/ARO-RP/pkg/$GOPACKAGE AsyncOperations,OpenShiftClusters,Subscriptions
|
||||
//go:generate go run ../../vendor/github.com/golang/mock/mockgen -destination=../util/mocks/$GOPACKAGE/$GOPACKAGE.go github.com/Azure/ARO-RP/pkg/$GOPACKAGE AsyncOperations,Billing,OpenShiftClusters,Subscriptions
|
||||
//go:generate go run ../../vendor/golang.org/x/tools/cmd/goimports -local=github.com/Azure/ARO-RP -e -w ../util/mocks/$GOPACKAGE/$GOPACKAGE.go
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -1162,6 +1162,29 @@ func (g *generator) database(databaseName string, addDependsOn bool) []*arm.Reso
|
|||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
|
||||
},
|
||||
},
|
||||
{
|
||||
Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
|
||||
SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
|
||||
Resource: &mgmtdocumentdb.SQLContainerResource{
|
||||
ID: to.StringPtr("Billing"),
|
||||
PartitionKey: &mgmtdocumentdb.ContainerPartitionKey{
|
||||
Paths: &[]string{
|
||||
"/id",
|
||||
},
|
||||
Kind: mgmtdocumentdb.PartitionKindHash,
|
||||
},
|
||||
},
|
||||
Options: map[string]*string{},
|
||||
},
|
||||
Name: to.StringPtr("[concat(parameters('databaseAccountName'), '/', " + databaseName + ", '/Billing')]"),
|
||||
Type: to.StringPtr("Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers"),
|
||||
Location: to.StringPtr("[resourceGroup().location]"),
|
||||
},
|
||||
APIVersion: apiVersions["documentdb"],
|
||||
DependsOn: []string{
|
||||
"[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), " + databaseName + ")]",
|
||||
},
|
||||
},
|
||||
{
|
||||
Resource: &mgmtdocumentdb.SQLContainerCreateUpdateParameters{
|
||||
SQLContainerCreateUpdateProperties: &mgmtdocumentdb.SQLContainerCreateUpdateProperties{
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package install
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
|
||||
)
|
||||
|
||||
func (i *Installer) createBillingRecord(ctx context.Context) error {
|
||||
_, err := i.billing.Create(ctx, &api.BillingDocument{
|
||||
ID: i.doc.ID,
|
||||
Key: i.doc.Key,
|
||||
ClusterResourceGroupIDKey: i.doc.ClusterResourceGroupIDKey,
|
||||
Billing: &api.Billing{
|
||||
TenantID: i.doc.OpenShiftCluster.Properties.ServicePrincipalProfile.TenantID,
|
||||
Location: i.doc.OpenShiftCluster.Location,
|
||||
},
|
||||
})
|
||||
// If create return a conflict, this means row is already present in database
|
||||
if err, ok := err.(*cosmosdb.Error); ok && err.StatusCode == http.StatusConflict {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
package install
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
|
||||
mock_database "github.com/Azure/ARO-RP/pkg/util/mocks/database"
|
||||
)
|
||||
|
||||
func TestCreateBillingEntry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockSubID := "11111111-1111-1111-1111-111111111111"
|
||||
mockTenantID := mockSubID
|
||||
location := "eastus"
|
||||
// controller := gomock.NewController(t)
|
||||
// defer controller.Finish()
|
||||
// billing := mock_database.NewMockBilling(controller)
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
openshiftdoc *api.OpenShiftClusterDocument
|
||||
mocks func(*test, *mock_database.MockBilling)
|
||||
wantError error
|
||||
}
|
||||
|
||||
for _, tt := range []*test{
|
||||
{
|
||||
name: "create a new billing entry",
|
||||
openshiftdoc: &api.OpenShiftClusterDocument{
|
||||
Key: "11111111-1111-1111-1111-111111111111",
|
||||
ClusterResourceGroupIDKey: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName", mockSubID),
|
||||
ID: mockSubID,
|
||||
OpenShiftCluster: &api.OpenShiftCluster{
|
||||
Properties: api.OpenShiftClusterProperties{
|
||||
ServicePrincipalProfile: api.ServicePrincipalProfile{
|
||||
TenantID: mockTenantID,
|
||||
},
|
||||
},
|
||||
Location: location,
|
||||
},
|
||||
},
|
||||
mocks: func(tt *test, billing *mock_database.MockBilling) {
|
||||
billingDoc := &api.BillingDocument{
|
||||
Key: tt.openshiftdoc.Key,
|
||||
ClusterResourceGroupIDKey: tt.openshiftdoc.ClusterResourceGroupIDKey,
|
||||
ID: mockSubID,
|
||||
Billing: &api.Billing{
|
||||
TenantID: tt.openshiftdoc.OpenShiftCluster.Properties.ServicePrincipalProfile.TenantID,
|
||||
Location: tt.openshiftdoc.OpenShiftCluster.Location,
|
||||
},
|
||||
}
|
||||
|
||||
billing.EXPECT().
|
||||
Create(gomock.Any(), billingDoc).
|
||||
Return(billingDoc, nil)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error on create a new billing entry",
|
||||
openshiftdoc: &api.OpenShiftClusterDocument{
|
||||
Key: "11111111-1111-1111-1111-111111111111",
|
||||
ClusterResourceGroupIDKey: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName", mockSubID),
|
||||
ID: mockSubID,
|
||||
OpenShiftCluster: &api.OpenShiftCluster{
|
||||
Properties: api.OpenShiftClusterProperties{
|
||||
ServicePrincipalProfile: api.ServicePrincipalProfile{
|
||||
TenantID: mockTenantID,
|
||||
},
|
||||
},
|
||||
Location: location,
|
||||
},
|
||||
},
|
||||
mocks: func(tt *test, billing *mock_database.MockBilling) {
|
||||
billingDoc := &api.BillingDocument{
|
||||
Key: tt.openshiftdoc.Key,
|
||||
ClusterResourceGroupIDKey: tt.openshiftdoc.ClusterResourceGroupIDKey,
|
||||
ID: mockSubID,
|
||||
Billing: &api.Billing{
|
||||
TenantID: tt.openshiftdoc.OpenShiftCluster.Properties.ServicePrincipalProfile.TenantID,
|
||||
Location: tt.openshiftdoc.OpenShiftCluster.Location,
|
||||
},
|
||||
}
|
||||
|
||||
billing.EXPECT().
|
||||
Create(gomock.Any(), billingDoc).
|
||||
Return(nil, tt.wantError)
|
||||
},
|
||||
wantError: fmt.Errorf("Error creating document"),
|
||||
},
|
||||
{
|
||||
name: "billing document already existing on DB on create",
|
||||
openshiftdoc: &api.OpenShiftClusterDocument{
|
||||
Key: "11111111-1111-1111-1111-111111111111",
|
||||
ClusterResourceGroupIDKey: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName", mockSubID),
|
||||
ID: mockSubID,
|
||||
OpenShiftCluster: &api.OpenShiftCluster{
|
||||
Properties: api.OpenShiftClusterProperties{
|
||||
ServicePrincipalProfile: api.ServicePrincipalProfile{
|
||||
TenantID: mockTenantID,
|
||||
},
|
||||
},
|
||||
Location: location,
|
||||
},
|
||||
},
|
||||
mocks: func(tt *test, billing *mock_database.MockBilling) {
|
||||
billingDoc := &api.BillingDocument{
|
||||
Key: tt.openshiftdoc.Key,
|
||||
ClusterResourceGroupIDKey: tt.openshiftdoc.ClusterResourceGroupIDKey,
|
||||
ID: mockSubID,
|
||||
Billing: &api.Billing{
|
||||
TenantID: tt.openshiftdoc.OpenShiftCluster.Properties.ServicePrincipalProfile.TenantID,
|
||||
Location: tt.openshiftdoc.OpenShiftCluster.Location,
|
||||
},
|
||||
}
|
||||
|
||||
billing.EXPECT().
|
||||
Create(gomock.Any(), billingDoc).
|
||||
Return(nil, &cosmosdb.Error{
|
||||
StatusCode: http.StatusConflict,
|
||||
})
|
||||
},
|
||||
wantError: nil,
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
billing := mock_database.NewMockBilling(controller)
|
||||
|
||||
tt.mocks(tt, billing)
|
||||
i := &Installer{
|
||||
log: logrus.NewEntry(logrus.StandardLogger()),
|
||||
doc: tt.openshiftdoc,
|
||||
billing: billing,
|
||||
}
|
||||
|
||||
err := i.createBillingRecord(ctx)
|
||||
if err != nil {
|
||||
if tt.wantError != err {
|
||||
t.Errorf("Error want (%s), having (%s)", tt.wantError.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -51,6 +51,7 @@ type Installer struct {
|
|||
log *logrus.Entry
|
||||
env env.Interface
|
||||
db database.OpenShiftClusters
|
||||
billing database.Billing
|
||||
doc *api.OpenShiftClusterDocument
|
||||
cipher encryption.Cipher
|
||||
fpAuthorizer autorest.Authorizer
|
||||
|
@ -84,7 +85,7 @@ type condition struct {
|
|||
}
|
||||
|
||||
// NewInstaller creates a new Installer
|
||||
func NewInstaller(ctx context.Context, log *logrus.Entry, env env.Interface, db database.OpenShiftClusters, doc *api.OpenShiftClusterDocument) (*Installer, error) {
|
||||
func NewInstaller(ctx context.Context, log *logrus.Entry, env env.Interface, db database.OpenShiftClusters, billing database.Billing, doc *api.OpenShiftClusterDocument) (*Installer, error) {
|
||||
r, err := azure.ParseResourceID(doc.OpenShiftCluster.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -114,6 +115,7 @@ func NewInstaller(ctx context.Context, log *logrus.Entry, env env.Interface, db
|
|||
log: log,
|
||||
env: env,
|
||||
db: db,
|
||||
billing: billing,
|
||||
cipher: cipher,
|
||||
doc: doc,
|
||||
fpAuthorizer: fpAuthorizer,
|
||||
|
@ -142,6 +144,7 @@ func (i *Installer) Install(ctx context.Context, installConfig *installconfig.In
|
|||
action(func(ctx context.Context) error {
|
||||
return i.installStorage(ctx, installConfig, platformCreds, image)
|
||||
}),
|
||||
action(i.createBillingRecord),
|
||||
action(i.installResources),
|
||||
action(i.createPrivateEndpoint),
|
||||
action(i.updateAPIIP),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/Azure/ARO-RP/pkg/database (interfaces: AsyncOperations,OpenShiftClusters,Subscriptions)
|
||||
// Source: github.com/Azure/ARO-RP/pkg/database (interfaces: AsyncOperations,Billing,OpenShiftClusters,Subscriptions)
|
||||
|
||||
// Package mock_database is a generated GoMock package.
|
||||
package mock_database
|
||||
|
@ -82,6 +82,74 @@ func (mr *MockAsyncOperationsMockRecorder) Patch(arg0, arg1, arg2 interface{}) *
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockAsyncOperations)(nil).Patch), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// MockBilling is a mock of Billing interface
|
||||
type MockBilling struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockBillingMockRecorder
|
||||
}
|
||||
|
||||
// MockBillingMockRecorder is the mock recorder for MockBilling
|
||||
type MockBillingMockRecorder struct {
|
||||
mock *MockBilling
|
||||
}
|
||||
|
||||
// NewMockBilling creates a new mock instance
|
||||
func NewMockBilling(ctrl *gomock.Controller) *MockBilling {
|
||||
mock := &MockBilling{ctrl: ctrl}
|
||||
mock.recorder = &MockBillingMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockBilling) EXPECT() *MockBillingMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Create mocks base method
|
||||
func (m *MockBilling) Create(arg0 context.Context, arg1 *api.BillingDocument) (*api.BillingDocument, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Create", arg0, arg1)
|
||||
ret0, _ := ret[0].(*api.BillingDocument)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Create indicates an expected call of Create
|
||||
func (mr *MockBillingMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockBilling)(nil).Create), arg0, arg1)
|
||||
}
|
||||
|
||||
// Get mocks base method
|
||||
func (m *MockBilling) Get(arg0 context.Context, arg1 string) (*api.BillingDocument, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get", arg0, arg1)
|
||||
ret0, _ := ret[0].(*api.BillingDocument)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get
|
||||
func (mr *MockBillingMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockBilling)(nil).Get), arg0, arg1)
|
||||
}
|
||||
|
||||
// MarkForDeletion mocks base method
|
||||
func (m *MockBilling) MarkForDeletion(arg0 context.Context, arg1 string) (*api.BillingDocument, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "MarkForDeletion", arg0, arg1)
|
||||
ret0, _ := ret[0].(*api.BillingDocument)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// MarkForDeletion indicates an expected call of MarkForDeletion
|
||||
func (mr *MockBillingMockRecorder) MarkForDeletion(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarkForDeletion", reflect.TypeOf((*MockBilling)(nil).MarkForDeletion), arg0, arg1)
|
||||
}
|
||||
|
||||
// MockOpenShiftClusters is a mock of OpenShiftClusters interface
|
||||
type MockOpenShiftClusters struct {
|
||||
ctrl *gomock.Controller
|
||||
|
|
Загрузка…
Ссылка в новой задаче