Skip to content

Commit 396cd5f

Browse files
authored
Emit empty features block for default azurerm provider without configuring one explicitly (#414)
* Emit empty features { } block for default azurerm provider * lint
1 parent 3373ef9 commit 396cd5f

File tree

8 files changed

+222
-0
lines changed

8 files changed

+222
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/bin/
2+
/node_modules/
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: azure-vnet-example
2+
description: A minimal Azure Native TypeScript Pulumi program
3+
runtime:
4+
name: nodejs
5+
options:
6+
packagemanager: npm
7+
config:
8+
pulumi:tags:
9+
value:
10+
pulumi:template: azure-typescript
11+
packages:
12+
vnet:
13+
source: terraform-module
14+
version: 0.1.6
15+
parameters:
16+
- Azure/avm-res-network-virtualnetwork/azurerm
17+
- 0.8.1
18+
- vnet
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Azure Native TypeScript Pulumi Template
2+
3+
This template provides a minimal, ready-to-go Pulumi program for deploying Azure resources using the Azure Native provider in TypeScript. It establishes a basic infrastructure stack that you can use as a foundation for more complex deployments.
4+
5+
## When to Use This Template
6+
7+
- You need a quick boilerplate for Azure Native deployments with Pulumi and TypeScript
8+
- You want to create a Resource Group and Storage Account as a starting point
9+
- You’re exploring Pulumi’s Azure Native SDK and TypeScript support
10+
11+
## Prerequisites
12+
13+
- An active Azure subscription
14+
- Node.js (LTS) installed
15+
- A Pulumi account and CLI already installed and configured
16+
- Azure credentials available (e.g., via `az login` or environment variables)
17+
18+
## Usage
19+
20+
Scaffold a new project from the Pulumi registry template:
21+
```bash
22+
pulumi new azure-typescript
23+
```
24+
25+
Follow the prompts to:
26+
1. Name your project and stack
27+
2. (Optionally) override the default Azure location
28+
29+
Once the project is created:
30+
```bash
31+
cd <your-project-name>
32+
pulumi config set azure-native:location <your-region>
33+
pulumi up
34+
```
35+
36+
## Project Layout
37+
38+
```
39+
.
40+
├── Pulumi.yaml # Project metadata & template configuration
41+
├── index.ts # Main Pulumi program defining resources
42+
├── package.json # Node.js dependencies and project metadata
43+
└── tsconfig.json # TypeScript compiler options
44+
```
45+
46+
## Configuration
47+
48+
Pulumi configuration lets you customize deployment parameters.
49+
50+
- **azure-native:location** (string)
51+
- Description: Azure region to provision resources in
52+
- Default: `WestUS2`
53+
54+
Set a custom location before deployment:
55+
```bash
56+
pulumi config set azure-native:location eastus
57+
```
58+
59+
## Resources Created
60+
61+
1. **Resource Group**: A container for all other resources
62+
2. **Storage Account**: A StorageV2 account with Standard_LRS SKU
63+
64+
## Outputs
65+
66+
After `pulumi up`, the following output is exported:
67+
- **primaryStorageKey**: The primary access key for the created Storage Account
68+
69+
Retrieve it with:
70+
```bash
71+
pulumi stack output primaryStorageKey
72+
```
73+
74+
## Next Steps
75+
76+
- Extend this template by adding more Azure Native resources (e.g., Networking, App Services)
77+
- Modularize your stack with Pulumi Components for reusable architectures
78+
- Integrate with CI/CD pipelines (GitHub Actions, Azure DevOps, etc.)
79+
80+
## Getting Help
81+
82+
If you have questions or run into issues:
83+
- Explore the Pulumi docs: https://www.pulumi.com/docs/
84+
- Join the Pulumi Community on Slack: https://pulumi-community.slack.com/
85+
- File an issue on the Pulumi Azure Native SDK GitHub: https://github.com/pulumi/pulumi-azure-native/issues
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as resources from "@pulumi/azure-native/resources";
2+
import * as vnet from "@pulumi/vnet";
3+
4+
const resourceGroup = new resources.ResourceGroup("resourceGroup", {
5+
location: "EastUS",
6+
});
7+
8+
// Create a virtual network in the resource group
9+
// requires ARM_SUBSCRIPTION_ID environment variable to be set
10+
const virtualNetwork = new vnet.Module("testvnet", {
11+
resource_group_name: resourceGroup.name,
12+
location: resourceGroup.location,
13+
address_space: ["10.0.0.0/16"],
14+
name: "testvnet",
15+
})
16+
17+
export const networkId = virtualNetwork.id;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "azure-vnet-example",
3+
"main": "index.ts",
4+
"devDependencies": {
5+
"@types/node": "^18",
6+
"typescript": "^5.0.0"
7+
},
8+
"dependencies": {
9+
"@pulumi/azure-native": "^2.0.0",
10+
"@pulumi/pulumi": "^3.113.0",
11+
"@pulumi/vnet": "file:sdks/vnet"
12+
}
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"outDir": "bin",
5+
"target": "es2020",
6+
"module": "commonjs",
7+
"moduleResolution": "node",
8+
"sourceMap": true,
9+
"experimentalDecorators": true,
10+
"pretty": true,
11+
"noFallthroughCasesInSwitch": true,
12+
"noImplicitReturns": true,
13+
"forceConsistentCasingInFileNames": true
14+
},
15+
"files": [
16+
"index.ts"
17+
]
18+
}

pkg/modprovider/server.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
emptypb "google.golang.org/protobuf/types/known/emptypb"
2929

30+
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
3031
"github.com/pulumi/pulumi/pkg/v3/resource/provider"
3132
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
3233
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
@@ -464,13 +465,36 @@ func (s *server) CheckConfig(
464465
}, nil
465466
}
466467

468+
// fixupProvidersConfigForAzureResourceManager ensures that the azurerm provider is configured
469+
// with an empty features block if it is required by the module but not explicitly configured
470+
func fixupProvidersConfigForAzureResourceManager(
471+
config map[string]resource.PropertyMap,
472+
providerVariables map[string]schema.PropertySpec,
473+
) map[string]resource.PropertyMap {
474+
475+
if len(providerVariables) == 0 {
476+
return config
477+
}
478+
_, requireAzureProvider := providerVariables["azurerm"]
479+
_, configuredAzure := config["azurerm"]
480+
if requireAzureProvider && !configuredAzure {
481+
config["azurerm"] = resource.PropertyMap{
482+
"features": resource.NewObjectProperty(resource.PropertyMap{}),
483+
}
484+
}
485+
486+
return config
487+
}
488+
467489
func (s *server) Diff(
468490
ctx context.Context,
469491
req *pulumirpc.DiffRequest,
470492
) (*pulumirpc.DiffResponse, error) {
471493
switch {
472494
case req.GetType() == string(moduleTypeToken(s.packageName)):
473495
providersConfig := cleanProvidersConfig(s.providerConfig)
496+
providerVariables := s.inferredModuleSchema.ProvidersConfig.Variables
497+
providersConfig = fixupProvidersConfigForAzureResourceManager(providersConfig, providerVariables)
474498
return s.moduleHandler.Diff(ctx, req, s.params.TFModuleSource, s.params.TFModuleVersion, providersConfig,
475499
s.inferredModuleSchema, s.moduleExecutor)
476500
default:
@@ -502,6 +526,8 @@ func (s *server) Create(
502526
switch {
503527
case req.GetType() == string(moduleTypeToken(s.packageName)):
504528
providersConfig := cleanProvidersConfig(s.providerConfig)
529+
providerVariables := s.inferredModuleSchema.ProvidersConfig.Variables
530+
providersConfig = fixupProvidersConfigForAzureResourceManager(providersConfig, providerVariables)
505531
return s.moduleHandler.Create(ctx, req, s.params.TFModuleSource, s.params.TFModuleVersion, providersConfig,
506532
s.inferredModuleSchema, s.packageName, s.moduleExecutor)
507533
default:
@@ -516,6 +542,8 @@ func (s *server) Update(
516542
switch {
517543
case req.GetType() == string(moduleTypeToken(s.packageName)):
518544
providersConfig := cleanProvidersConfig(s.providerConfig)
545+
providerVariables := s.inferredModuleSchema.ProvidersConfig.Variables
546+
providersConfig = fixupProvidersConfigForAzureResourceManager(providersConfig, providerVariables)
519547
return s.moduleHandler.Update(ctx, req, s.params.TFModuleSource, s.params.TFModuleVersion, providersConfig,
520548
s.inferredModuleSchema, s.packageName, s.moduleExecutor)
521549
default:
@@ -530,6 +558,8 @@ func (s *server) Delete(
530558
switch {
531559
case req.GetType() == string(moduleTypeToken(s.packageName)):
532560
providersConfig := cleanProvidersConfig(s.providerConfig)
561+
providerVariables := s.inferredModuleSchema.ProvidersConfig.Variables
562+
providersConfig = fixupProvidersConfigForAzureResourceManager(providersConfig, providerVariables)
533563
return s.moduleHandler.Delete(ctx, req, s.packageName,
534564
s.params.TFModuleSource, s.params.TFModuleVersion,
535565
s.inferredModuleSchema, providersConfig, s.moduleExecutor)
@@ -554,6 +584,8 @@ func (s *server) Read(
554584
switch {
555585
case req.GetType() == string(moduleTypeToken(s.packageName)):
556586
providersConfig := cleanProvidersConfig(s.providerConfig)
587+
providerVariables := s.inferredModuleSchema.ProvidersConfig.Variables
588+
providersConfig = fixupProvidersConfigForAzureResourceManager(providersConfig, providerVariables)
557589
return s.moduleHandler.Read(ctx, req, s.packageName,
558590
s.params.TFModuleSource, s.params.TFModuleVersion,
559591
s.inferredModuleSchema, providersConfig, s.moduleExecutor)

tests/examples_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package tests
1616

1717
import (
18+
"os"
1819
"path/filepath"
1920
"testing"
2021

@@ -64,6 +65,42 @@ func Test_RdsExample(t *testing.T) {
6465
)
6566
}
6667

68+
func Test_Azure_VirtualNetworkExample_NoExplicitProvider(t *testing.T) {
69+
t.Parallel()
70+
71+
if _, ci := os.LookupEnv("CI"); !ci {
72+
t.Skip("Skipping Azure tests in local runs without credentials.")
73+
}
74+
75+
if _, ok := os.LookupEnv("ARM_SUBSCRIPTION_ID"); !ok {
76+
t.Skip("Skipping AzureRM tests without ARM_SUBSCRIPTION_ID set.")
77+
}
78+
79+
tw := newTestWriter(t)
80+
localProviderBinPath := ensureCompiledProvider(t)
81+
// Module written to support the test.
82+
testProgram, err := filepath.Abs(filepath.Join("../", "examples", "azure-vnet-example"))
83+
require.NoError(t, err)
84+
localPath := opttest.LocalProviderPath("terraform-module", filepath.Dir(localProviderBinPath))
85+
integrationTest := newPulumiTest(t, testProgram, localPath)
86+
87+
// Generate package
88+
pulumiPackageAdd(t, integrationTest, localProviderBinPath,
89+
"Azure/avm-res-network-virtualnetwork/azurerm", "0.8.1", "vnet")
90+
91+
integrationTest.Up(t, optup.Diff(),
92+
optup.ErrorProgressStreams(tw),
93+
optup.ProgressStreams(tw),
94+
)
95+
96+
integrationTest.Preview(t,
97+
optpreview.Diff(),
98+
optpreview.ExpectNoChanges(),
99+
optpreview.ErrorProgressStreams(tw),
100+
optpreview.ProgressStreams(tw),
101+
)
102+
}
103+
67104
func Test_EksExample(t *testing.T) {
68105
t.Parallel()
69106
localProviderBinPath := ensureCompiledProvider(t)

0 commit comments

Comments
 (0)