move billing functionality into pkg/util/billing and add e2e storage account functionality

Co-authored-by: Julien Stroheker <juliens@microsoft.com>
Co-authored-by: Jim Minter <jminter@redhat.com>
This commit is contained in:
Jack Quincy 2020-04-11 12:16:28 -05:00 коммит произвёл Jim Minter
Родитель dd357469f7
Коммит c87c7c1baa
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 0730CBDA10D1A2D3
16 изменённых файлов: 777 добавлений и 150 удалений

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

@ -28,5 +28,22 @@ const (
type SubscriptionProperties struct {
MissingFields
TenantID string `json:"tenantId,omitempty"`
TenantID string `json:"tenantId,omitempty"`
AccountOwner *AccountOwnerProfile `json:"accountOwner,omitempty"`
RegisteredFeatures []RegisteredFeatureProfile `json:"registeredFeatures,omitempty"`
}
// AccountOwnerProfile represents the subscription account owner information
type AccountOwnerProfile struct {
MissingFields
Email string `json:"email,omitempty"`
}
// RegisteredFeatureProfile represents the features registered to the subscription
type RegisteredFeatureProfile struct {
MissingFields
Name string `json:"name,omitempty"`
State string `json:"state,omitempty"`
}

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

@ -84,7 +84,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, ocb.db.Billing, doc)
m, err := openshiftcluster.NewManager(log, ocb.env, ocb.db.OpenShiftClusters, ocb.db.Billing, ocb.db.Subscriptions, doc)
if err != nil {
return ocb.endLease(ctx, log, stop, doc, api.ProvisioningStateFailed, err)
}

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

@ -10,7 +10,6 @@ import (
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
"github.com/Azure/ARO-RP/pkg/env"
"github.com/Azure/ARO-RP/pkg/util/stringutils"
"github.com/Azure/ARO-RP/pkg/util/subnet"
@ -92,7 +91,6 @@ func (m *Manager) Delete(ctx context.Context) error {
(detailedErr.StatusCode == http.StatusForbidden || detailedErr.StatusCode == http.StatusNotFound) {
err = nil
}
if err != nil {
return err
}
@ -107,10 +105,5 @@ func (m *Manager) Delete(ctx context.Context) error {
}
}
m.log.Printf("updating billing record with deletion time")
_, err = m.billing.MarkForDeletion(ctx, m.doc.ID)
if cosmosdb.IsErrorStatusCode(err, http.StatusNotFound) {
return nil
}
return err
return m.billing.Delete(ctx, m.doc)
}

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

@ -15,6 +15,7 @@ import (
"github.com/Azure/ARO-RP/pkg/util/acrtoken"
pkgacrtoken "github.com/Azure/ARO-RP/pkg/util/acrtoken"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/features"
"github.com/Azure/ARO-RP/pkg/util/billing"
"github.com/Azure/ARO-RP/pkg/util/dns"
"github.com/Azure/ARO-RP/pkg/util/keyvault"
"github.com/Azure/ARO-RP/pkg/util/privateendpoint"
@ -25,7 +26,7 @@ type Manager struct {
log *logrus.Entry
env env.Interface
db database.OpenShiftClusters
billing database.Billing
billing billing.Manager
fpAuthorizer autorest.Authorizer
ocDynamicValidator validate.OpenShiftClusterDynamicValidator
@ -41,7 +42,7 @@ type Manager struct {
doc *api.OpenShiftClusterDocument
}
func NewManager(log *logrus.Entry, _env env.Interface, db database.OpenShiftClusters, billing database.Billing, doc *api.OpenShiftClusterDocument) (*Manager, error) {
func NewManager(log *logrus.Entry, _env env.Interface, db database.OpenShiftClusters, billingDB database.Billing, sub database.Subscriptions, doc *api.OpenShiftClusterDocument) (*Manager, error) {
r, err := azure.ParseResourceID(doc.OpenShiftCluster.ID)
if err != nil {
return nil, err
@ -70,11 +71,16 @@ func NewManager(log *logrus.Entry, _env env.Interface, db database.OpenShiftClus
}
}
billingManager, err := billing.NewManager(_env, billingDB, sub, log)
if err != nil {
return nil, err
}
m := &Manager{
log: log,
env: _env,
db: db,
billing: billing,
billing: billingManager,
fpAuthorizer: fpAuthorizer,
ocDynamicValidator: validate.NewOpenShiftClusterDynamicValidator(_env),

12
pkg/env/dev.go поставляемый
Просмотреть файл

@ -263,3 +263,15 @@ func (d *dev) CreateARMResourceGroupRoleAssignment(ctx context.Context, fpAuthor
return err
}
func (d *dev) E2EStorageAccountName() string {
return "arov4e2eint"
}
func (d *dev) E2EStorageAccountRGName() string {
return "global-infra"
}
func (d *dev) E2EStorageAccountSubID() string {
return "0cc1cafa-578f-4fa5-8d6b-ddfd8d82e6ea"
}

3
pkg/env/env.go поставляемый
Просмотреть файл

@ -51,6 +51,9 @@ type Interface interface {
Zones(vmSize string) ([]string, error)
ACRResourceID() string
ACRName() string
E2EStorageAccountName() string
E2EStorageAccountRGName() string
E2EStorageAccountSubID() string
}
func NewEnv(ctx context.Context, log *logrus.Entry) (Interface, error) {

3
pkg/env/int.go поставляемый
Просмотреть файл

@ -21,6 +21,9 @@ func newInt(ctx context.Context, log *logrus.Entry, instancemetadata instancemet
p.fpServicePrincipalID = "71cfb175-ea3a-444e-8c03-b119b2752ce4"
p.clustersGenevaLoggingEnvironment = "Test"
p.clustersGenevaLoggingConfigVersion = "2.2"
p.e2eStorageAccountName = "arov4e2eint"
p.e2eStorageAccountRGName = "global-infra"
p.e2eStorageAccountSubID = "0cc1cafa-578f-4fa5-8d6b-ddfd8d82e6ea"
return p, nil
}

20
pkg/env/prod.go поставляемый
Просмотреть файл

@ -55,6 +55,10 @@ type prod struct {
clustersGenevaLoggingConfigVersion string
clustersGenevaLoggingEnvironment string
e2eStorageAccountName string
e2eStorageAccountRGName string
e2eStorageAccountSubID string
log *logrus.Entry
}
@ -117,6 +121,10 @@ func newProd(ctx context.Context, log *logrus.Entry, instancemetadata instanceme
p.clustersGenevaLoggingPrivateKey = clustersGenevaLoggingPrivateKey
p.clustersGenevaLoggingCertificate = clustersGenevaLoggingCertificates[0]
p.e2eStorageAccountName = "v4aroe2e"
p.e2eStorageAccountRGName = "global"
p.e2eStorageAccountSubID = "0923c7de-9fca-4d9e-baf3-131d0c5b2ea4"
if p.ACRResourceID() != "" { // TODO: ugh!
acrResource, err := azure.ParseResourceID(p.ACRResourceID())
if err != nil {
@ -353,3 +361,15 @@ func (p *prod) Zones(vmSize string) ([]string, error) {
}
return zones, nil
}
func (p *prod) E2EStorageAccountName() string {
return p.e2eStorageAccountName
}
func (p *prod) E2EStorageAccountRGName() string {
return p.e2eStorageAccountRGName
}
func (p *prod) E2EStorageAccountSubID() string {
return p.e2eStorageAccountSubID
}

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

@ -5,26 +5,8 @@ package install
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
return i.billing.Create(ctx, i.doc)
}

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

@ -5,150 +5,57 @@ package install
import (
"context"
"fmt"
"net/http"
"errors"
"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"
mock_billing "github.com/Azure/ARO-RP/pkg/util/mocks/billing"
)
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{
for _, tt := range []struct {
name string
mocks func(*mock_billing.MockManager)
wantErr string
}{
{
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,
},
}
name: "manager create is called and doesn't return an error when create doesn't return an error",
mocks: func(billing *mock_billing.MockManager) {
billing.EXPECT().
Create(gomock.Any(), billingDoc).
Return(billingDoc, nil)
Create(gomock.Any(), &api.OpenShiftClusterDocument{}).
Return(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,
},
}
name: "manager create is called and returns an error on create returning an error",
mocks: func(billing *mock_billing.MockManager) {
billing.EXPECT().
Create(gomock.Any(), billingDoc).
Return(nil, tt.wantError)
Create(gomock.Any(), &api.OpenShiftClusterDocument{}).
Return(errors.New("random error"))
},
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,
wantErr: "random error",
},
} {
t.Run(tt.name, func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
billing := mock_database.NewMockBilling(controller)
tt.mocks(tt, billing)
billing := mock_billing.NewMockManager(controller)
tt.mocks(billing)
i := &Installer{
log: logrus.NewEntry(logrus.StandardLogger()),
doc: tt.openshiftdoc,
doc: &api.OpenShiftClusterDocument{},
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())
}
if err != nil && err.Error() != tt.wantErr ||
err == nil && tt.wantErr != "" {
t.Error(err)
}
})
}

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

@ -41,6 +41,7 @@ import (
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/features"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/network"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/storage"
"github.com/Azure/ARO-RP/pkg/util/billing"
"github.com/Azure/ARO-RP/pkg/util/dns"
"github.com/Azure/ARO-RP/pkg/util/encryption"
"github.com/Azure/ARO-RP/pkg/util/keyvault"
@ -55,7 +56,7 @@ type Installer struct {
log *logrus.Entry
env env.Interface
db database.OpenShiftClusters
billing database.Billing
billing billing.Manager
doc *api.OpenShiftClusterDocument
cipher encryption.Cipher
fpAuthorizer autorest.Authorizer
@ -93,7 +94,7 @@ type condition struct {
}
// NewInstaller creates a new Installer
func NewInstaller(ctx context.Context, log *logrus.Entry, _env env.Interface, db database.OpenShiftClusters, billing database.Billing, doc *api.OpenShiftClusterDocument) (*Installer, error) {
func NewInstaller(ctx context.Context, log *logrus.Entry, _env env.Interface, db database.OpenShiftClusters, billing billing.Manager, doc *api.OpenShiftClusterDocument) (*Installer, error) {
r, err := azure.ParseResourceID(doc.OpenShiftCluster.ID)
if err != nil {
return nil, err

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

@ -0,0 +1,181 @@
package billing
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
azstorage "github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/api"
"github.com/Azure/ARO-RP/pkg/database"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
"github.com/Azure/ARO-RP/pkg/env"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/storage"
"github.com/Azure/ARO-RP/pkg/util/feature"
)
const (
tenantIDMSFT = "72f988bf-86f1-41af-91ab-2d7cd011db47"
tenantIDAME = "33e01921-4d64-4f8c-a055-5bdaffd5e33d"
// featureSaveAROTestConfig is the feature in the subscription that is used
// to indicate if we need to save ARO cluster config into the E2E
// StorageAccount
featureSaveAROTestConfig = "Microsoft.RedHatOpenShift/SaveAROTestConfig"
)
type Manager interface {
Create(context.Context, *api.OpenShiftClusterDocument) error
Delete(context.Context, *api.OpenShiftClusterDocument) error
}
type manager struct {
env env.Interface
storageClient *azstorage.Client
billingDB database.Billing
subDB database.Subscriptions
log *logrus.Entry
}
func NewManager(_env env.Interface, billing database.Billing, sub database.Subscriptions, log *logrus.Entry) (Manager, error) {
var storageClient *azstorage.Client
if _, ok := _env.(env.Dev); !ok {
localFPAuthorizer, err := _env.FPAuthorizer(_env.TenantID(), azure.PublicCloud.ResourceManagerEndpoint)
if err != nil {
return nil, err
}
e2estorage := storage.NewAccountsClient(_env.E2EStorageAccountSubID(), localFPAuthorizer)
if err != nil {
return nil, err
}
keys, err := e2estorage.ListKeys(context.Background(), _env.E2EStorageAccountRGName(), _env.E2EStorageAccountName(), "")
if err != nil {
return nil, err
}
key := *(*keys.Keys)[0].Value
client, err := azstorage.NewBasicClient(_env.E2EStorageAccountName(), key)
if err != nil {
return nil, err
}
storageClient = &client
}
return &manager{
env: _env,
storageClient: storageClient,
subDB: sub,
billingDB: billing,
log: log,
}, nil
}
func (m *manager) Create(ctx context.Context, doc *api.OpenShiftClusterDocument) error {
billingDoc, err := m.billingDB.Create(ctx, &api.BillingDocument{
ID: doc.ID,
Key: doc.Key,
ClusterResourceGroupIDKey: doc.ClusterResourceGroupIDKey,
Billing: &api.Billing{
TenantID: doc.OpenShiftCluster.Properties.ServicePrincipalProfile.TenantID,
Location: doc.OpenShiftCluster.Location,
},
})
if err, ok := err.(*cosmosdb.Error); ok &&
err.StatusCode == http.StatusConflict {
m.log.Print("billing record already present in DB")
return nil
}
if err != nil {
return err
}
if e2eErr := m.createOrUpdateE2EBlob(ctx, billingDoc); e2eErr != nil {
m.log.Warnf("createOrUpdateE2EBlob failed: %s", e2eErr)
}
return nil
}
func (m *manager) Delete(ctx context.Context, doc *api.OpenShiftClusterDocument) error {
m.log.Printf("updating billing record with deletion time")
billingDoc, err := m.billingDB.MarkForDeletion(ctx, doc.ID)
if cosmosdb.IsErrorStatusCode(err, http.StatusNotFound) {
return nil
}
if err != nil {
return err
}
if e2eErr := m.createOrUpdateE2EBlob(ctx, billingDoc); e2eErr != nil {
// We are not failing the operation if we cannot write to e2e storage account, just warning
m.log.Warnf("createOrUpdateE2EBlob failed: %s", e2eErr)
}
return nil
}
// isSubscriptionRegisteredForE2E returns true if the subscription has the
// "Microsoft.RedHatOpenShift/SaveAROTestConfig" feature registered
func isSubscriptionRegisteredForE2E(sub *api.SubscriptionProperties) bool {
if sub.TenantID == tenantIDMSFT || sub.TenantID == tenantIDAME {
return feature.IsRegisteredForFeature(sub, featureSaveAROTestConfig)
}
return false
}
// createOrUpdateE2Eblob create a copy of the billing document in the e2e
// storage account. This is used later on by the billing e2e
func (m *manager) createOrUpdateE2EBlob(ctx context.Context, doc *api.BillingDocument) error {
//skip updating the storage account if this is a dev scenario
if _, ok := m.env.(env.Dev); ok {
return nil
}
// Validate if E2E Feature is registered
resource, err := azure.ParseResourceID(doc.Key)
if err != nil {
return err
}
subDocument, err := m.subDB.Get(ctx, resource.SubscriptionID)
if err != nil {
return err
}
if !isSubscriptionRegisteredForE2E(subDocument.Subscription.Properties) {
return nil
}
blobclient := m.storageClient.GetBlobService()
containerName := strings.ToLower("bill-" + doc.Billing.Location + "-" + resource.ResourceGroup + "-" + resource.ResourceName)
if len(containerName) > 63 {
containerName = containerName[:63]
}
containerRef := blobclient.GetContainerReference(containerName)
_, err = containerRef.CreateIfNotExists(nil)
if err != nil {
return err
}
blobRef := containerRef.GetBlobReference("billingentity")
b, err := json.Marshal(doc)
if err != nil {
return err
}
return blobRef.CreateBlockBlobFromReader(bytes.NewReader(b), nil)
}

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

@ -0,0 +1,413 @@
package billing
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"errors"
"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 TestIsSubscriptionRegisteredForE2E(t *testing.T) {
mockSubID := "11111111-1111-1111-1111-111111111111"
for _, tt := range []struct {
name string
sub api.SubscriptionProperties
want bool
}{
{
name: "empty",
},
{
name: "sub without feature flag registered and not internal tenant",
sub: api.SubscriptionProperties{
TenantID: mockSubID,
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: "RandomFeature",
State: "Registered",
},
},
},
},
{
name: "sub with feature flag registered and not internal tenant",
sub: api.SubscriptionProperties{
TenantID: mockSubID,
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: featureSaveAROTestConfig,
State: "Registered",
},
},
},
},
{
name: "AME internal tenant and feature flag not registered",
sub: api.SubscriptionProperties{
TenantID: tenantIDAME,
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: "RandomFeature",
State: "Registered",
},
},
},
},
{
name: "MSFT internal tenant and feature flag not registered",
sub: api.SubscriptionProperties{
TenantID: tenantIDMSFT,
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: "RandomFeature",
State: "Registered",
},
},
},
},
{
name: "AME internal tenant and feature flag registered",
sub: api.SubscriptionProperties{
TenantID: tenantIDAME,
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: featureSaveAROTestConfig,
State: "Registered",
},
},
},
want: true,
},
{
name: "MSFT internal tenant and feature flag registered",
sub: api.SubscriptionProperties{
TenantID: tenantIDMSFT,
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: featureSaveAROTestConfig,
State: "Registered",
},
},
},
want: true,
},
} {
t.Run(tt.name, func(t *testing.T) {
got := isSubscriptionRegisteredForE2E(&tt.sub)
if got != tt.want {
t.Error(got)
}
})
}
}
func TestDelete(t *testing.T) {
ctx := context.Background()
mockSubID := "11111111-1111-1111-1111-111111111111"
mockTenantID := mockSubID
location := "eastus"
type test struct {
name string
openshiftdoc *api.OpenShiftClusterDocument
mocks func(*test, *mock_database.MockBilling, *mock_database.MockSubscriptions)
wantErr string
}
// Can't add tests for billing storage because there isn't an interface on
// the azure storage clients.
for _, tt := range []*test{
{
name: "successful mark for deletion on billing entity, with a subscription not registered for e2e",
openshiftdoc: &api.OpenShiftClusterDocument{
Key: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName/providers/microsoft.redhatopenshift/openshiftclusters/clusterName", mockSubID),
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, subscription *mock_database.MockSubscriptions) {
billingDoc := &api.BillingDocument{
Key: tt.openshiftdoc.Key,
ClusterResourceGroupIDKey: tt.openshiftdoc.ClusterResourceGroupIDKey,
ID: mockSubID,
Billing: &api.Billing{
TenantID: mockTenantID,
Location: location,
},
}
billing.EXPECT().
MarkForDeletion(gomock.Any(), billingDoc.ID).
Return(billingDoc, nil)
subscription.EXPECT().
Get(gomock.Any(), mockSubID).
Return(&api.SubscriptionDocument{
Subscription: &api.Subscription{
Properties: &api.SubscriptionProperties{
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: featureSaveAROTestConfig,
State: "NotRegistered",
},
},
},
},
}, nil)
},
},
{
name: "no error on mark for deletion on billing entry that is not found",
openshiftdoc: &api.OpenShiftClusterDocument{
Key: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName/providers/microsoft.redhatopenshift/openshiftclusters/clusterName", mockSubID),
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, subscription *mock_database.MockSubscriptions) {
billing.EXPECT().
MarkForDeletion(gomock.Any(), tt.openshiftdoc.ID).
Return(nil, &cosmosdb.Error{
StatusCode: 404,
})
},
},
{
name: "error on mark for deletion on billing entry",
openshiftdoc: &api.OpenShiftClusterDocument{
Key: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName/providers/microsoft.redhatopenshift/openshiftclusters/clusterName", mockSubID),
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, subscription *mock_database.MockSubscriptions) {
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().
MarkForDeletion(gomock.Any(), tt.openshiftdoc.ID).
Return(billingDoc, errors.New("random error"))
},
wantErr: "random error",
},
} {
t.Run(tt.name, func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
log := logrus.NewEntry(logrus.StandardLogger())
billingDB := mock_database.NewMockBilling(controller)
subsDB := mock_database.NewMockSubscriptions(controller)
tt.mocks(tt, billingDB, subsDB)
m := &manager{
log: log,
billingDB: billingDB,
subDB: subsDB,
}
err := m.Delete(ctx, tt.openshiftdoc)
if err != nil && err.Error() != tt.wantErr ||
err == nil && tt.wantErr != "" {
t.Error(err)
}
})
}
}
func TestCreate(t *testing.T) {
ctx := context.Background()
mockSubID := "11111111-1111-1111-1111-111111111111"
mockTenantID := mockSubID
location := "eastus"
type test struct {
name string
openshiftdoc *api.OpenShiftClusterDocument
mocks func(*test, *mock_database.MockBilling, *mock_database.MockSubscriptions)
wantErr string
}
// Can't add tests for billing storage because there isn't an interface on
// the azure storage clients.
for _, tt := range []*test{
{
name: "create a new billing entry with a subscription not registered for e2e",
openshiftdoc: &api.OpenShiftClusterDocument{
Key: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName/providers/microsoft.redhatopenshift/openshiftclusters/clusterName", mockSubID),
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, subscription *mock_database.MockSubscriptions) {
billingDoc := &api.BillingDocument{
Key: tt.openshiftdoc.Key,
ClusterResourceGroupIDKey: tt.openshiftdoc.ClusterResourceGroupIDKey,
ID: mockSubID,
Billing: &api.Billing{
TenantID: mockTenantID,
Location: location,
},
}
billing.EXPECT().
Create(gomock.Any(), billingDoc).
Return(billingDoc, nil)
subscription.EXPECT().
Get(gomock.Any(), mockSubID).
Return(&api.SubscriptionDocument{
Subscription: &api.Subscription{
Properties: &api.SubscriptionProperties{
RegisteredFeatures: []api.RegisteredFeatureProfile{
{
Name: featureSaveAROTestConfig,
State: "NotRegistered",
},
},
},
},
}, nil)
},
},
{
name: "error on create a new billing entry",
openshiftdoc: &api.OpenShiftClusterDocument{
Key: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName/providers/microsoft.redhatopenshift/openshiftclusters/clusterName", mockSubID),
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, subscription *mock_database.MockSubscriptions) {
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, errors.New("random error"))
},
wantErr: "random error",
},
{
name: "billing document already existing on DB on create",
openshiftdoc: &api.OpenShiftClusterDocument{
Key: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName/providers/microsoft.redhatopenshift/openshiftclusters/clusterName", mockSubID),
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, subscription *mock_database.MockSubscriptions) {
billingDoc := &api.BillingDocument{
Key: fmt.Sprintf("/subscriptions/%s/resourcegroups/rgName/providers/microsoft.redhatopenshift/openshiftclusters/clusterName", mockSubID),
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,
})
},
},
} {
t.Run(tt.name, func(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
log := logrus.NewEntry(logrus.StandardLogger())
billingDB := mock_database.NewMockBilling(controller)
subsDB := mock_database.NewMockSubscriptions(controller)
tt.mocks(tt, billingDB, subsDB)
m := &manager{
log: log,
billingDB: billingDB,
subDB: subsDB,
}
err := m.Create(ctx, tt.openshiftdoc)
if err != nil && err.Error() != tt.wantErr ||
err == nil && tt.wantErr != "" {
t.Error(err)
}
})
}
}

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

@ -0,0 +1,7 @@
package billing
// 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/util/$GOPACKAGE Manager
//go:generate go run ../../../vendor/golang.org/x/tools/cmd/goimports -local=github.com/Azure/ARO-RP -e -w ../../util/mocks/$GOPACKAGE/$GOPACKAGE.go

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

@ -0,0 +1,17 @@
package feature
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"github.com/Azure/ARO-RP/pkg/api"
)
func IsRegisteredForFeature(sub *api.SubscriptionProperties, feature string) bool {
for _, f := range sub.RegisteredFeatures {
if f.Name == feature && f.State == "Registered" {
return true
}
}
return false
}

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

@ -0,0 +1,65 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/Azure/ARO-RP/pkg/util/billing (interfaces: Manager)
// Package mock_billing is a generated GoMock package.
package mock_billing
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
api "github.com/Azure/ARO-RP/pkg/api"
)
// MockManager is a mock of Manager interface
type MockManager struct {
ctrl *gomock.Controller
recorder *MockManagerMockRecorder
}
// MockManagerMockRecorder is the mock recorder for MockManager
type MockManagerMockRecorder struct {
mock *MockManager
}
// NewMockManager creates a new mock instance
func NewMockManager(ctrl *gomock.Controller) *MockManager {
mock := &MockManager{ctrl: ctrl}
mock.recorder = &MockManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockManager) EXPECT() *MockManagerMockRecorder {
return m.recorder
}
// Create mocks base method
func (m *MockManager) Create(arg0 context.Context, arg1 *api.OpenShiftClusterDocument) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Create indicates an expected call of Create
func (mr *MockManagerMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), arg0, arg1)
}
// Delete mocks base method
func (m *MockManager) Delete(arg0 context.Context, arg1 *api.OpenShiftClusterDocument) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete
func (mr *MockManagerMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockManager)(nil).Delete), arg0, arg1)
}