* 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:
Julien Stroheker 2020-03-11 11:18:34 -04:00 коммит произвёл GitHub
Родитель 44e43af806
Коммит 8a51a88b57
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 812 добавлений и 9 удалений

Просмотреть файл

@ -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

16
pkg/api/billing.go Normal file
Просмотреть файл

@ -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),

124
pkg/database/billing.go Normal file
Просмотреть файл

@ -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{

30
pkg/install/billing.go Normal file
Просмотреть файл

@ -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
}

155
pkg/install/billing_test.go Normal file
Просмотреть файл

@ -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