Skip to content

Commit

Permalink
refactor entitlement
Browse files Browse the repository at this point in the history
  • Loading branch information
parametalol committed Sep 12, 2023
1 parent 39bc123 commit 0e18ba5
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 36 deletions.
83 changes: 63 additions & 20 deletions internal/dinosaur/pkg/services/quota/ams_quota_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ func newBaseQuotaReservedResourceResourceBuilder() amsv1.ReservedResourceBuilder
return rr
}

var supportedAMSBillingModels = map[string]struct{}{
string(amsv1.BillingModelMarketplace): {},
string(amsv1.BillingModelStandard): {},
string(amsv1.BillingModelMarketplaceAWS): {},
var supportedAMSBillingModels = map[string]bool{
string(amsv1.BillingModelMarketplace): true,
string(amsv1.BillingModelStandard): true,
string(amsv1.BillingModelMarketplaceAWS): true,
}

// CheckIfQuotaIsDefinedForInstanceType ...
Expand Down Expand Up @@ -71,7 +71,7 @@ func (q amsQuotaService) hasConfiguredQuotaCost(organizationID string, quotaType
for _, qc := range quotaCosts {
if qc.Allowed() > 0 {
for _, rr := range qc.RelatedResources() {
if _, isCompatibleBillingModel := supportedAMSBillingModels[rr.BillingModel()]; isCompatibleBillingModel {
if supportedAMSBillingModels[rr.BillingModel()] {
return true, nil
}
foundUnsupportedBillingModel = rr.BillingModel()
Expand Down Expand Up @@ -214,6 +214,44 @@ func (q amsQuotaService) DeleteQuota(subscriptionID string) *errors.ServiceError
return nil
}

func mapAllowedQuotaCosts(quotaCosts []*amsv1.QuotaCost) (map[amsv1.BillingModel][]*amsv1.QuotaCost, error) {
costsMap := make(map[amsv1.BillingModel][]*amsv1.QuotaCost)
var foundUnsupportedBillingModel string
for _, qc := range quotaCosts {
// When an SKU entitlement expires in AMS, the allowed value for that quota cost is set back to 0.
// If the allowed value is 0 and consumed is greater than this, that denotes that the SKU entitlement
// has expired and is no longer active.
if qc.Allowed() == 0 {
continue
}
for _, rr := range qc.RelatedResources() {
bm := amsv1.BillingModel(rr.BillingModel())
if supportedAMSBillingModels[rr.BillingModel()] {
costsMap[bm] = append(costsMap[bm], qc)
} else {
foundUnsupportedBillingModel = rr.BillingModel()
}

}
}
if len(costsMap) == 0 && foundUnsupportedBillingModel != "" {
return nil, errors.GeneralError("found unsupported allowed billing models, the last one is %s", foundUnsupportedBillingModel)
}
return costsMap, nil
}

func cloudAccountIsActive(cloudQuotas []*amsv1.QuotaCost, central *dbapi.CentralRequest) bool {
for _, qc := range cloudQuotas {
for _, account := range qc.CloudAccounts() {
if account.CloudAccountID() == central.CloudAccountID &&
account.CloudProviderID() == central.CloudProvider {
return true
}
}
}
return false
}

// IsQuotaEntitlementActive checks if an organisation has a SKU entitlement and that entitlement is still active in AMS.
func (q amsQuotaService) IsQuotaEntitlementActive(central *dbapi.CentralRequest) (bool, error) {
org, err := q.amsClient.GetOrganisationFromExternalID(central.OrganisationID)
Expand All @@ -227,25 +265,30 @@ func (q amsQuotaService) IsQuotaEntitlementActive(central *dbapi.CentralRequest)
return false, errors.InsufficientQuotaError("%v: error getting quotas for product %s", err, quotaType.GetProduct())
}

// When an SKU entitlement expires in AMS, the allowed value for that quota cost is set back to 0.
// If the allowed value is 0 and consumed is greater than this, that denotes that the SKU entitlement
// has expired and is no longer active.
for _, qc := range quotaCosts {
quotasMap, err := mapAllowedQuotaCosts(quotaCosts)
if err != nil {
svcErr := errors.ToServiceError(err)
return false, errors.NewWithCause(svcErr.Code, svcErr, "product %s has no allowed billing models", quotaType.GetProduct())
}

isCloudAccount := central.CloudAccountID != "" && central.CloudProvider == awsCloudProvider

entitled := qc.Allowed() > 0
available := qc.Allowed() - qc.Consumed()
entitled :=
// Entitlement is active if there's allowed quota for standard billing model...
!isCloudAccount && len(quotasMap[amsv1.BillingModelStandard]) > 0 ||
// or there is cloud quota and the original cloud account is still active.
cloudAccountIsActive(quotasMap[amsv1.BillingModelMarketplaceAWS], central)

if !entitled {
glog.Infof("Quota no longer entitled for organisation %q (quotaid: %q, consumed: %q, allowed: %q)",
if !entitled {
glog.Infof("Quota no longer entitled for organisation %q", org.ID)
return false, nil
}
for _, qc := range quotaCosts {
if qc.Consumed() > qc.Allowed() {
glog.Warningf("Organisation %q has exceeded their quota allowance (quotaid: %q, consumed %q, allowed: %q)",
org.ID, qc.QuotaID(), qc.Consumed(), qc.Allowed())
} else {
if available < 0 {
glog.Warningf("Organisation %q has exceeded their quota allowance (quotaid: %q, consumed %q, allowed: %q)",
org.ID, qc.QuotaID(), qc.Consumed(), qc.Allowed())
}
return true, nil
}
}

return false, nil
return true, nil
}
143 changes: 127 additions & 16 deletions internal/dinosaur/pkg/services/quota/ams_quota_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,16 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
InstanceType: "standard",
}

cloudCentral := &dbapi.CentralRequest{
InstanceType: "standard",
CloudProvider: awsCloudProvider,
CloudAccountID: "cloudAccountID",
}
const not_allowed = 0
const not_consumed = 0
const allowed = 1
const consumed = 1

tests := []struct {
name string
fields fields
Expand All @@ -953,14 +963,14 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
wantErrMsg string
}{
{
name: "returns true for single available quota cost",
name: "returns true for single allowed quota cost",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeTestQuotaCost(resourceName, organizationID, 1, 0, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
}, nil
},
},
Expand All @@ -970,14 +980,14 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
wantErr: false,
},
{
name: "returns true for single available quota cost for eval instance",
name: "returns true for single allowed quota cost for eval instance",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeTestQuotaCost(resourceName, organizationID, 1, 0, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
}, nil
},
},
Expand All @@ -1004,15 +1014,15 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
wantErr: false,
},
{
name: "returns true for several available quota costs",
name: "returns true for several allowed quota costs",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeTestQuotaCost(resourceName, organizationID, 1, 0, t),
makeTestQuotaCost(resourceName, organizationID, 1, 0, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
}, nil
},
},
Expand All @@ -1022,15 +1032,15 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
wantErr: false,
},
{
name: "returns true for one of several available quota costs",
name: "returns true for one of several allowed quota costs",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeTestQuotaCost(resourceName, organizationID, 1, 0, t),
makeTestQuotaCost(resourceName, organizationID, 0, 1, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
makeStandardTestQuotaCost(resourceName, organizationID, not_allowed, consumed, t),
}, nil
},
},
Expand All @@ -1047,8 +1057,8 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeTestQuotaCost(resourceName, organizationID, 1, 3, t),
makeTestQuotaCost(resourceName, organizationID, 1, 3, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, consumed*2, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, consumed*3, t),
}, nil
},
},
Expand All @@ -1058,15 +1068,51 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
wantErr: false,
},
{
name: "returns false for no quota cost available",
name: "returns false for no quota cost allowed",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeStandardTestQuotaCost(resourceName, organizationID, not_allowed, consumed, t),
makeStandardTestQuotaCost(resourceName, organizationID, not_allowed, consumed, t),
}, nil
},
},
},
args: standardCentral,
want: false,
wantErr: false,
},
{
name: "returns false if cloud account has no allowed cost, but standard has",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeCloudTestQuotaCost(resourceName, organizationID, not_allowed, consumed, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, consumed, t),
}, nil
},
},
},
args: cloudCentral,
want: false,
wantErr: false,
},
{
name: "returns false if standard account has no allowed cost, but cloud has",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeTestQuotaCost(resourceName, organizationID, 0, 1, t),
makeTestQuotaCost(resourceName, organizationID, 0, 1, t),
makeCloudTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
makeStandardTestQuotaCost(resourceName, organizationID, not_allowed, not_consumed, t),
}, nil
},
},
Expand All @@ -1075,6 +1121,63 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
want: false,
wantErr: false,
},
{
name: "returns false if cloud account has no allowed cost, neither standard has",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeCloudTestQuotaCost(resourceName, organizationID, not_allowed, not_consumed, t),
makeStandardTestQuotaCost(resourceName, organizationID, not_allowed, not_consumed, t),
}, nil
},
},
},
args: cloudCentral,
want: false,
wantErr: false,
},
{
name: "returns true if cloud account has allowed cost, and standard has",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeCloudTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
makeStandardTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
}, nil
},
},
},
args: cloudCentral,
want: true,
wantErr: false,
},
{
name: "returns false if cloud account has no active account",
fields: fields{
centralConfig: defaultCentralConfig,
amsClient: &ocm.ClientMock{
GetOrganisationFromExternalIDFunc: makeOrganizationFromExtID,
GetQuotaCostsForProductFunc: func(organizationID, resourceName, product string) ([]*v1.QuotaCost, error) {
return []*v1.QuotaCost{
makeCloudTestQuotaCost(resourceName, organizationID, allowed, not_consumed, t),
}, nil
},
},
},
args: &dbapi.CentralRequest{
InstanceType: "standard",
CloudProvider: awsCloudProvider,
CloudAccountID: "unsubscribedCloudAccountID",
},
want: false,
wantErr: false,
},
{
name: "returns an error when it fails to get quota costs from ams",
fields: fields{
Expand Down Expand Up @@ -1109,13 +1212,21 @@ func Test_amsQuotaService_IsQuotaEntitlementActive(t *testing.T) {
}
}

func makeTestQuotaCost(resourceName string, organizationID string, allowed, consumed int, t *testing.T) *v1.QuotaCost {
func makeStandardTestQuotaCost(resourceName string, organizationID string, allowed int, consumed int, t *testing.T) *v1.QuotaCost {
rrbq := v1.NewRelatedResource().BillingModel(string(v1.BillingModelStandard)).Product(string(ocm.RHACSProduct)).ResourceName(resourceName).Cost(1)
qcb, err := v1.NewQuotaCost().Allowed(allowed).Consumed(consumed).OrganizationID(organizationID).RelatedResources(rrbq).Build()
require.NoError(t, err)
return qcb
}

func makeCloudTestQuotaCost(resourceName string, organizationID string, allowed int, consumed int, t *testing.T) *v1.QuotaCost {
cloudAccount := v1.NewCloudAccount().CloudAccountID("cloudAccountID").CloudProviderID(awsCloudProvider)
rrbq := v1.NewRelatedResource().BillingModel(string(v1.BillingModelMarketplaceAWS)).Product(string(ocm.RHACSProduct)).ResourceName(resourceName).Cost(1)
qcb, err := v1.NewQuotaCost().Allowed(allowed).Consumed(consumed).OrganizationID(organizationID).RelatedResources(rrbq).CloudAccounts(cloudAccount).Build()
require.NoError(t, err)
return qcb
}

func makeOrganizationFromExtID(externalId string) (*v1.Organization, error) {
org, _ := v1.NewOrganization().ID(fmt.Sprintf("fake-org-id-%s", externalId)).Build()
return org, nil
Expand Down

0 comments on commit 0e18ba5

Please sign in to comment.