Skip to content

Commit 5a5c17a

Browse files
tsimacekTomas Simacek
and
Tomas Simacek
authored
v0.11.1 release (#12)
Fixed issue with deploying private plans Fixed issue with listing available marketplace plans Co-authored-by: Tomas Simacek <tsimacek@pruestorage.com>
1 parent 4cb6a84 commit 5a5c17a

File tree

13 files changed

+184
-53
lines changed

13 files changed

+184
-53
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.11.1 (Mar, 13, 2024)
2+
* Fixed issue with deploying private plans
3+
* Fixed issue with listing available marketplace plans
4+
15
## 0.11.0 (Sep, 20, 2024)
26
* Added support for V10MP2R2 model
37
* Removed support for specifying version plan by specific version (e.g `6.6.4`)

cbs/acceptance/environment.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ const (
2828
// when using Service Principal auth.
2929
EnvTfAccSkipUserPrincipalAuth = "TF_ACC_SKIP_USER_PRINCIPAL_AUTH"
3030

31+
// Environment variable controlling if the Azure acceptance tests
32+
// for service principal should be run. This testing is not available
33+
// when using OIDC auth.
34+
EnvTfAccSkipServicePrincipalAuth = "TF_ACC_SKIP_SERVICE_PRINCIPAL_AUTH"
35+
3136
// Environment variable for controlling the Azure acceptance tests
3237
// related to deploying an CBS Fusion app from an App Definition
3338
EnvTfAccAzureSkipFusionAppId = "TC_ACC_SKIP_AZURE_FUSION_APP_ID"

cbs/data_azure_plans.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"time"
3131

3232
"github.com/hashicorp/go-version"
33+
"github.com/hashicorp/terraform-plugin-log/tflog"
3334
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
3435
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
3536
)
@@ -125,7 +126,7 @@ func (a PlanByVersion) Len() int { return len(a) }
125126
func (a PlanByVersion) Less(i, j int) bool { return a[i].Version.LessThan(&a[j].Version) }
126127
func (a PlanByVersion) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
127128

128-
var plan_name_regexp = regexp.MustCompile(`^[\w]+_([\d]+)_([\d]+)_(x)$`)
129+
var plan_name_regexp = regexp.MustCompile(`^[\w]+_([\d]+)_([\d]+)_([\d]+|x)$`)
129130

130131
// Retrieve a plan information from Azure DefaultTemplate artifact
131132
func GetPlanFromTemplateJson(data []byte) (*Plan, error) {
@@ -148,17 +149,22 @@ func GetPlanFromTemplateJson(data []byte) (*Plan, error) {
148149
return &unmarshalled_plan, nil
149150
}
150151

151-
func versionPlans(plans []Plan) ([]VersionedPlan, error) {
152+
func versionPlans(ctx context.Context, plans []Plan) ([]VersionedPlan, error) {
152153
var versioned_plans []VersionedPlan
153154
for _, plan := range plans {
154155
match := plan_name_regexp.FindStringSubmatch(plan.Name)
156+
// Plan does not match our standard name schema but we want it to show it but treat it as 0.0.0
157+
if len(match) < 4 {
158+
tflog.Debug(ctx, fmt.Sprintf("Found plan '%s' not matching standard scheme", plan.Name))
159+
match = []string{"0", "0", "0", "0"}
160+
}
155161
patch := match[3]
156162
if patch == "x" {
157163
patch = "99"
158164
}
159165
version, err := version.NewVersion(fmt.Sprintf("%s.%s.%s", match[1], match[2], patch))
160166
if err != nil {
161-
return nil, fmt.Errorf("Unable to parse version string in plan name: %s", plan.Name)
167+
return nil, fmt.Errorf("unable to parse version string in plan name: %s", plan.Name)
162168
}
163169
versioned_plans = append(versioned_plans, VersionedPlan{*version, plan})
164170
}
@@ -175,13 +181,15 @@ func QueryMarketplaceForPlans(ctx context.Context) ([]VersionedPlan, error) {
175181

176182
var template_plans []Plan
177183
for _, response_result := range productSummary.Results {
184+
// Filter out non Cloud Block Store offers, unfortunatelly the API does not have offer/product ID available so we need
185+
// to take a look at the inner plans to filter out offers we do not want
186+
if len(response_result.Plans) == 0 || !strings.HasPrefix(*response_result.Plans[0].UniquePlanID, marketplacePlanIdPrefix) {
187+
tflog.Debug(ctx, fmt.Sprintf("Skipping non Cloud Block Store offer %s", *response_result.DisplayName))
188+
continue
189+
}
190+
tflog.Debug(ctx, fmt.Sprintf("Processing plans under offer %s", *response_result.DisplayName))
178191
for _, response_plan := range response_result.Plans {
179-
if !strings.HasPrefix(*response_plan.PlanID, "cbs_azure") {
180-
// Exclude any plans which aren't the Pure Cloud Block Store on Azure offering.
181-
// E.g. the Pure Fusion Storage Endpoint Collection offerings.
182-
continue
183-
}
184-
192+
tflog.Debug(ctx, fmt.Sprintf("Processing plan %s", *response_plan.PlanID))
185193
for _, response_artifact := range response_plan.Artifacts {
186194
if *response_artifact.Name != "DefaultTemplate" {
187195
continue
@@ -202,7 +210,7 @@ func QueryMarketplaceForPlans(ctx context.Context) ([]VersionedPlan, error) {
202210
}
203211
}
204212

205-
return versionPlans(template_plans)
213+
return versionPlans(ctx, template_plans)
206214
}
207215

208216
func dataSourceAzurePlansRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {

cbs/internal/cloud/azure.go

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141

4242
vaultSecret "github.com/Azure/azure-sdk-for-go/services/keyvault/v7.1/keyvault"
4343
vaultManagement "github.com/Azure/azure-sdk-for-go/services/preview/keyvault/mgmt/2020-04-01-preview/keyvault"
44+
"github.com/manicminer/hamilton/environments"
4445

4546
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/managedapplications"
4647
"github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources"
@@ -128,6 +129,31 @@ func buildAzureClient(ctx context.Context, userConfig AzureConfig) (AzureClientA
128129
SupportsAzureCliToken: true,
129130
}
130131

132+
useOIDCAuth := false
133+
useMSAL := false
134+
if os.Getenv("TF_ACC_USE_OIDC") != "" {
135+
useOIDCAuth = true
136+
}
137+
138+
// Temporary workaround for Vault migration until we do PURE-384171
139+
// which is essentialy to use azidentity for auth, however that requires
140+
// more significant refactoring of how we use the SDK. The migration guide
141+
// https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/azidentity/MIGRATION.md
142+
if useOIDCAuth {
143+
builder.SubscriptionID = os.Getenv("AZURE_SUBSCRIPTION_ID")
144+
builder.TenantID = os.Getenv("AZURE_TENANT_ID")
145+
builder.ClientID = os.Getenv("AZURE_CLIENT_ID")
146+
builder.SupportsOIDCAuth = true
147+
builder.IDTokenRequestURL = os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL")
148+
builder.IDTokenRequestToken = os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
149+
// OIDC is not without Microsoft Graph
150+
// https://github.com/hashicorp/go-azure-helpers/blob/d43458d68a62a2d14d08ee543e67a7a2d301ad8d/authentication/auth_method_oidc.go#L40
151+
builder.UseMicrosoftGraph = true
152+
// OIDC only supports MSAL tokens
153+
// https://github.com/hashicorp/go-azure-helpers/blob/d43458d68a62a2d14d08ee543e67a7a2d301ad8d/authentication/auth_method_oidc.go#L48
154+
useMSAL = true
155+
}
156+
131157
config, err := builder.Build()
132158
if err != nil {
133159
return nil, err
@@ -149,15 +175,32 @@ func buildAzureClient(ctx context.Context, userConfig AzureConfig) (AzureClientA
149175
}
150176

151177
sender := sender.BuildSender("cbs")
152-
auth, err := config.GetADALToken(ctx, sender, oauthConfig, env.TokenAudience)
153-
if err != nil {
154-
return nil, err
178+
179+
var auth, graphAuth, vaultAuthorizer autorest.Authorizer = nil, nil, nil
180+
var authErr error = nil
181+
if useMSAL {
182+
auth, authErr = config.GetMSALToken(ctx, environments.ResourceManagerPublic, sender, oauthConfig, env.TokenAudience)
183+
} else {
184+
auth, authErr = config.GetADALToken(ctx, sender, oauthConfig, env.TokenAudience)
155185
}
156-
graphAuth, err := config.GetADALToken(ctx, sender, oauthConfig, env.GraphEndpoint)
157-
if err != nil {
158-
return nil, err
186+
if authErr != nil {
187+
return nil, authErr
188+
}
189+
190+
if useMSAL {
191+
graphAuth, authErr = config.GetMSALToken(ctx, environments.MsGraphGlobal, sender, oauthConfig, env.GraphEndpoint)
192+
} else {
193+
graphAuth, authErr = config.GetADALToken(ctx, sender, oauthConfig, env.GraphEndpoint)
194+
}
195+
if authErr != nil {
196+
return nil, authErr
197+
}
198+
199+
if useMSAL {
200+
vaultAuthorizer = config.MSALBearerAuthorizerCallback(ctx, environments.KeyVaultPublic, sender, oauthConfig, env.KeyVaultEndpoint)
201+
} else {
202+
vaultAuthorizer = config.ADALBearerAuthorizerCallback(ctx, sender, oauthConfig)
159203
}
160-
vaultAuthorizer := config.ADALBearerAuthorizerCallback(ctx, sender, oauthConfig)
161204

162205
vaultSecretClient := vaultSecret.New()
163206
vaultSecretClient.Authorizer = vaultAuthorizer

cbs/provider_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ func TestAccProvider_azureCLIAuth(t *testing.T) {
194194
}
195195

196196
func TestAccProvider_azureServicePrincipalAuth(t *testing.T) {
197+
if os.Getenv(acceptance.EnvTfAccSkipServicePrincipalAuth) != "" {
198+
t.Skip("Skipping due to env variable set")
199+
}
200+
197201
if os.Getenv("TF_ACC") == "" {
198202
return
199203
}

cbs/resource_array_azure.go

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,28 @@ const (
5757
defaultPlanVersion = "1.0.0"
5858
)
5959

60+
const marketplacePlanIdPrefix = "purestoragemarketplaceadmin.pure_cloud_block_store_product_deployment"
61+
62+
var staticResourceList = []string{
63+
"Microsoft.Solutions/applications",
64+
"Microsoft.Resources/tags",
65+
"Microsoft.Compute/virtualMachines",
66+
"Microsoft.Network/networkInterfaces",
67+
"Microsoft.DocumentDB/databaseAccounts",
68+
"Microsoft.Storage/storageAccounts",
69+
"Microsoft.ManagedIdentity/userAssignedIdentities",
70+
"Microsoft.KeyVault/vaults",
71+
"Microsoft.Network/loadBalancers",
72+
"Microsoft.Network/publicIPAddresses",
73+
"Microsoft.Compute/disks",
74+
"Microsoft.Compute/virtualMachines/extensions",
75+
"Microsoft.Network/networkSecurityGroups",
76+
"Microsoft.Network/applicationSecurityGroups",
77+
"Microsoft.Compute/capacityReservationGroups",
78+
"Microsoft.Compute/capacityReservationGroups/capacityReservations",
79+
"Microsoft.Resources/deploymentScripts",
80+
}
81+
6082
var azureParams = []interface{}{
6183
"arrayName",
6284
"licenseKey",
@@ -407,43 +429,61 @@ func GetResourcesFromTemplateJson(data []byte) ([]string, error) {
407429
return resources, nil
408430
}
409431

410-
func GetPlanArtifacts(ctx context.Context, planName string) (map[string]*appcatalog.Artifact, error) {
432+
func GetPlanArtifacts(ctx context.Context, plan Plan) (map[string]*appcatalog.Artifact, error) {
411433
productSummary, err := GetProductSummary(ctx)
412434
if err != nil {
413435
return nil, err
414436
}
415437

416438
for _, response_result := range productSummary.Results {
439+
// Filter out non Cloud Block Store offers, unfortunatelly the API does not have offer/product ID available so we need
440+
// to take a look at the inner plans to filter out offers we do not want
441+
if len(response_result.Plans) == 0 || !strings.HasPrefix(*response_result.Plans[0].UniquePlanID, marketplacePlanIdPrefix) {
442+
tflog.Debug(ctx, fmt.Sprintf("Skipping non Cloud Block Store offer %s", *response_result.DisplayName))
443+
continue
444+
}
445+
tflog.Debug(ctx, fmt.Sprintf("Processing plans under offer %s", *response_result.DisplayName))
417446
for _, response_plan := range response_result.Plans {
418-
if !strings.HasPrefix(*response_plan.PlanID, "cbs_azure") {
447+
if plan.Name != *response_plan.PlanID {
448+
tflog.Debug(ctx, fmt.Sprintf("Skipping non matching plan %s", *response_plan.PlanID))
419449
continue
420450
}
451+
tflog.Debug(ctx, fmt.Sprintf("Found matching plan %s", *response_plan.PlanID))
421452

422453
artifacts := make(map[string]*appcatalog.Artifact)
423-
var plan *Plan
454+
var templatePlan *Plan
424455
for _, response_artifact := range response_plan.Artifacts {
425456
artifacts[*response_artifact.Name] = response_artifact
426-
// we get the current plan from Default Template
457+
// Get the plan properties from the DefaultTemplate artifact to verify integrity
427458
if *response_artifact.Name == "DefaultTemplate" {
428459
template_data, err := downloadToBuffer(*response_artifact.URI)
429460
if err != nil {
430-
return artifacts, err
461+
return nil, err
431462
}
432463

433-
plan, err = GetPlanFromTemplateJson(template_data)
464+
templatePlan, err = GetPlanFromTemplateJson(template_data)
434465
if err != nil {
435-
return artifacts, err
466+
return nil, err
436467
}
437468
}
438469
}
439-
440-
if plan.Name == planName {
441-
return artifacts, nil
470+
// Verify the integrity of the artifact and that it matches our expectations
471+
if templatePlan.Name != plan.Name {
472+
return nil, fmt.Errorf("mismatch between planID in marketplace DefaultTemplate")
473+
}
474+
if templatePlan.Product != plan.Product {
475+
return nil, fmt.Errorf("mismatch between product in marketplace response and DefaultTemplate")
442476
}
477+
if templatePlan.Publisher != plan.Publisher {
478+
return nil, fmt.Errorf("mismatch between publisher in marketplace response and DefaultTemplate")
479+
}
480+
if templatePlan.Version != plan.Version {
481+
return nil, fmt.Errorf("mismatch between plan version in marketplace response and DefaultTemplate")
482+
}
483+
return artifacts, nil
443484
}
444485
}
445-
446-
return nil, fmt.Errorf("plan %s not found to get artifacts", planName)
486+
return nil, fmt.Errorf("could not find plan %s in marketplace in order to get Cloud Block Store artifacts", plan.Name)
447487
}
448488

449489
func GetProductSummary(ctx context.Context) (appcatalog.SearchClientGetResponse, error) {
@@ -460,12 +500,8 @@ func GetResourceListFromUiDefinitionUrl(url string) ([]string, error) {
460500
return GetResourcesFromTemplateJson(template_data)
461501
}
462502

463-
func getPlanResources(ctx context.Context, p interface{}) ([]string, error) {
464-
planInterface := p.([]interface{})[0]
465-
plan := planInterface.(map[string]interface{})
466-
planName := plan["name"].(string)
467-
468-
artifacts, err := GetPlanArtifacts(ctx, planName)
503+
func getPlanResources(ctx context.Context, plan Plan) ([]string, error) {
504+
artifacts, err := GetPlanArtifacts(ctx, plan)
469505
if err != nil {
470506
return nil, err
471507
}
@@ -516,9 +552,17 @@ func getResourcesForCurrentDeployment(ctx context.Context, azureClient cloud.Azu
516552
}
517553

518554
if planParam, ok := d.GetOk("plan"); ok {
519-
resources, err = getPlanResources(ctx, planParam)
555+
// We rely on Terraform framework here that this will always be convertible, otherwise we would panic
556+
planDecoded := planParam.([]interface{})[0].(map[string]interface{})
557+
plan := Plan{
558+
Name: planDecoded["name"].(string),
559+
Product: planDecoded["product"].(string),
560+
Publisher: planDecoded["publisher"].(string),
561+
Version: planDecoded["version"].(string),
562+
}
563+
resources, err = getPlanResources(ctx, plan)
520564
if err != nil {
521-
return nil, fmt.Errorf("failed to retrieve resource list based on plan %+v", err)
565+
return nil, fmt.Errorf("could not get resource list from plan %s: %+v", plan.Name, err)
522566
}
523567
}
524568

@@ -639,13 +683,17 @@ func resourceArrayAzureCreate(ctx context.Context, d *schema.ResourceData, m int
639683

640684
parameters.Identity = expandIdentityObject(identities)
641685

686+
// Fetch the list of resources which will be deployed. We rely on createUiDefinition.json artifact which should
687+
// be available through the marketplace api in case of public plans. In case that the plan is not available in
688+
// the marketplace API for any reason we fall back to a statically defined list.
689+
resources, err := getResourcesForCurrentDeployment(ctx, azureClient, d)
690+
if err != nil {
691+
tflog.Warn(ctx, fmt.Sprintf("Could not get resource list for current deployment, falling back to static resource list: %s", err))
692+
resources = staticResourceList
693+
}
694+
642695
tagsMap := make(map[string]interface{})
643696
if v, ok := d.GetOk("tags"); ok {
644-
resources, err := getResourcesForCurrentDeployment(ctx, azureClient, d)
645-
if err != nil {
646-
return diag.Errorf("cannot get resource list %+v", err)
647-
}
648-
649697
tags := v.(map[string]interface{})
650698
for _, tag := range resources {
651699
copyMap := make(map[string]interface{})
@@ -657,11 +705,6 @@ func resourceArrayAzureCreate(ctx context.Context, d *schema.ResourceData, m int
657705
}
658706

659707
if v, ok := d.GetOk("resource_tags"); ok {
660-
resources, err := getResourcesForCurrentDeployment(ctx, azureClient, d)
661-
if err != nil {
662-
return diag.Errorf("cannot get resource list %+v", err)
663-
}
664-
665708
resource_tags := v.([]interface{})
666709
for _, resource_tag := range resource_tags {
667710
resource := resource_tag.(map[string]interface{})["resource"].(string)

docs/data-sources/azure_plans.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ description: |-
88

99
# cbs_plan_azure (Data Source)
1010

11-
Provides plan specific details specified by plan version parameter. The version needs to be specified in format of version prefix e.g. `6.5.x` or `6.6.x` and similar. Specific versions like `6.6.10` are not supported as only the latest version in a given release line is svailable in the marketplace offer for deployment.
11+
Provides plan specific details specified by plan version parameter. The version needs to be specified in format of version prefix e.g. `6.7.x` or `6.8.x` and similar. Specific versions like `6.8.2` are not supported as only the latest version in a given release line is svailable in the marketplace offer for deployment.
1212

1313
## Example Usage
1414
```hcl

docs/resources/array_aws.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ resource "cbs_array_aws" "cbs_example" {
2727
2828
array_name = "terraform-example-instance"
2929
30-
deployment_template_url = "https://s3.amazonaws.com/awsmp-fulfillment-cf-templates-prod/4ea2905b-7939-4ee0-a521-d5c2fcb41214/e52106cb-f0af-4409-88af-34a93018f603.template"
30+
deployment_template_url = "https://s3.amazonaws.com/awsmp-fulfillment-cf-templates-prod/4ea2905b-7939-4ee0-a521-d5c2fcb41214/9e8d86fec7df43a285273e60b3f40e1f.template"
3131
deployment_role_arn = "arn:aws:iam::xxxxxxxxxxxx:role/example_role"
3232
3333
log_sender_domain = "example-company.org"

docs/resources/array_azure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ resource "azurerm_key_vault" "cbs_key_vault" {
6161
}
6262
6363
data "cbs_plan_azure" "version_plan" {
64-
plan_version = "6.6.x"
64+
plan_version = "6.8.x"
6565
}
6666
6767
resource "cbs_array_azure" "azure_instance" {

examples/aws_array/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ terraform {
22
required_providers {
33
cbs = {
44
source = "PureStorage-OpenConnect/cbs"
5-
version = "~> 0.11.0"
5+
version = "~> 0.11.1"
66
}
77
}
88
}

0 commit comments

Comments
 (0)