Skip to content

Commit 25c778a

Browse files
authored
Implement autonaming protocol to set the name input variable of a module to the resource name (#417)
* Automatically set the name input of a module to the resource name * Implement autonaming protocol * autoname using unique resource names
1 parent 47eff8c commit 25c778a

File tree

11 files changed

+258
-22
lines changed

11 files changed

+258
-22
lines changed
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import * as resources from "@pulumi/azure-native/resources";
22
import * as vnet from "@pulumi/vnet";
3+
import * as pulumi from "@pulumi/pulumi";
4+
5+
const cfg = new pulumi.Config();
6+
const rgName = cfg.get("rg") ?? pulumi.getStack();
37

48
const resourceGroup = new resources.ResourceGroup("resourceGroup", {
9+
resourceGroupName: rgName,
510
location: "EastUS",
611
});
712

@@ -11,7 +16,6 @@ const virtualNetwork = new vnet.Module("testvnet", {
1116
resource_group_name: resourceGroup.name,
1217
location: resourceGroup.location,
1318
address_space: ["10.0.0.0/16"],
14-
name: "testvnet",
1519
})
1620

17-
export const networkId = virtualNetwork.id;
21+
export const networkId = virtualNetwork.name;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"module": {
3+
"mymod": {
4+
"source": "Azure/avm-res-network-virtualnetwork/azurerm",
5+
"version": "0.8.1"
6+
}
7+
}
8+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"module": {
3+
"testvnet": {
4+
"address_space": [
5+
"10.0.0.0/16"
6+
],
7+
"location": "eastus",
8+
"name": "testvnet",
9+
"providers": {
10+
"azurerm": "azurerm"
11+
},
12+
"resource_group_name": "RESOURCE_GROUP-resource-group",
13+
"source": "Azure/avm-res-network-virtualnetwork/azurerm",
14+
"version": "0.8.1"
15+
}
16+
},
17+
"output": {
18+
"internal_output_is_secret_name": {
19+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.name)))}"
20+
},
21+
"internal_output_is_secret_peerings": {
22+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.peerings)))}"
23+
},
24+
"internal_output_is_secret_resource": {
25+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.resource)))}"
26+
},
27+
"internal_output_is_secret_resource_id": {
28+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.resource_id)))}"
29+
},
30+
"internal_output_is_secret_subnets": {
31+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.subnets)))}"
32+
},
33+
"name": {
34+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.name)))}"
35+
},
36+
"peerings": {
37+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.peerings)))}"
38+
},
39+
"resource": {
40+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.resource)))}"
41+
},
42+
"resource_id": {
43+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.resource_id)))}"
44+
},
45+
"subnets": {
46+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.subnets)))}"
47+
}
48+
},
49+
"provider": {
50+
"azurerm": {
51+
"features": {}
52+
}
53+
}
54+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"module": {
3+
"mymod": {
4+
"source": "Azure/avm-res-network-virtualnetwork/azurerm",
5+
"version": "0.8.1"
6+
}
7+
}
8+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"module": {
3+
"testvnet": {
4+
"address_space": [
5+
"10.0.0.0/16"
6+
],
7+
"location": "eastus",
8+
"name": "testvnet",
9+
"providers": {
10+
"azurerm": "azurerm"
11+
},
12+
"resource_group_name": "RESOURCE_GROUP-resource-group",
13+
"source": "Azure/avm-res-network-virtualnetwork/azurerm",
14+
"version": "0.8.1"
15+
}
16+
},
17+
"output": {
18+
"internal_output_is_secret_name": {
19+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.name)))}"
20+
},
21+
"internal_output_is_secret_peerings": {
22+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.peerings)))}"
23+
},
24+
"internal_output_is_secret_resource": {
25+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.resource)))}"
26+
},
27+
"internal_output_is_secret_resource_id": {
28+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.resource_id)))}"
29+
},
30+
"internal_output_is_secret_subnets": {
31+
"value": "${jsondecode(issensitive(jsonencode(module.testvnet.subnets)))}"
32+
},
33+
"name": {
34+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.name)))}"
35+
},
36+
"peerings": {
37+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.peerings)))}"
38+
},
39+
"resource": {
40+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.resource)))}"
41+
},
42+
"resource_id": {
43+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.resource_id)))}"
44+
},
45+
"subnets": {
46+
"value": "${jsondecode(nonsensitive(jsonencode(module.testvnet.subnets)))}"
47+
}
48+
},
49+
"provider": {
50+
"azurerm": {
51+
"features": {}
52+
}
53+
}
54+
}

pkg/modprovider/module.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"google.golang.org/grpc/codes"
2222
"google.golang.org/protobuf/types/known/emptypb"
23+
"google.golang.org/protobuf/types/known/structpb"
2324

2425
"github.com/pulumi/pulumi/pkg/v3/resource/provider"
2526
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
@@ -64,9 +65,44 @@ func moduleTypeToken(pkgName packageName) tokens.Type {
6465
func (h *moduleHandler) Check(
6566
_ context.Context,
6667
req *pulumirpc.CheckRequest,
68+
moduleSchema *InferredModuleSchema,
6769
) (*pulumirpc.CheckResponse, error) {
70+
news := make(map[string]*structpb.Value)
71+
if req.News != nil && req.News.Fields != nil {
72+
news = req.News.Fields
73+
}
74+
75+
_, nameInputProvided := news["name"]
76+
inputProperty, hasNameInput := moduleSchema.Inputs["name"]
77+
if hasNameInput && inputProperty.Type == "string" && !nameInputProvided {
78+
olds := make(map[string]*structpb.Value)
79+
if req.Olds != nil && req.Olds.Fields != nil {
80+
olds = req.Olds.Fields
81+
}
82+
83+
if previouslySetName, ok := olds["name"]; ok {
84+
news["name"] = previouslySetName
85+
} else {
86+
// if the module schema specifies a name input property and it is not set by the user,
87+
// then we need to set it to the name of the resource urn.
88+
urn := urn.URN(req.GetUrn())
89+
prefix := urn.Name() + "-"
90+
autoname, err := resource.NewUniqueName(req.RandomSeed, prefix, 0, 0, nil)
91+
contract.AssertNoErrorf(err, "NewUniqueName should not fail in Check")
92+
if req.Autonaming != nil {
93+
switch req.Autonaming.Mode {
94+
case pulumirpc.CheckRequest_AutonamingOptions_ENFORCE, pulumirpc.CheckRequest_AutonamingOptions_PROPOSE:
95+
contract.Assertf(req.Autonaming.ProposedName != "", "expected proposed name to be non-empty: %v", req.Autonaming)
96+
autoname = req.Autonaming.ProposedName
97+
}
98+
}
99+
100+
news["name"] = structpb.NewStringValue(autoname)
101+
}
102+
}
103+
68104
return &pulumirpc.CheckResponse{
69-
Inputs: req.News,
105+
Inputs: &structpb.Struct{Fields: news},
70106
}, nil
71107
}
72108

@@ -275,7 +311,7 @@ func (h *moduleHandler) applyModuleOperation(
275311
executor,
276312
)
277313
if err != nil {
278-
return nil, nil, fmt.Errorf("Failed preparing tofu sandbox: %w", err)
314+
return nil, nil, fmt.Errorf("failed preparing sandbox: %w", err)
279315
}
280316

281317
logger := newResourceLogger(h.hc, urn)

pkg/modprovider/server.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ func (s *server) Construct(
414414
_ context.Context,
415415
req *pulumirpc.ConstructRequest,
416416
) (*pulumirpc.ConstructResponse, error) {
417-
return nil, fmt.Errorf("Unsupported type: %q", req.GetType())
417+
return nil, fmt.Errorf("unsupported type: %q", req.GetType())
418418
}
419419

420420
func (s *server) Check(
@@ -423,7 +423,7 @@ func (s *server) Check(
423423
) (*pulumirpc.CheckResponse, error) {
424424
switch {
425425
case req.GetType() == string(moduleTypeToken(s.packageName)):
426-
return s.moduleHandler.Check(ctx, req)
426+
return s.moduleHandler.Check(ctx, req, s.inferredModuleSchema)
427427
default:
428428
return nil, fmt.Errorf("[Check]: type %q is not supported yet", req.GetType())
429429
}
@@ -513,9 +513,10 @@ func (s *server) Handshake(
513513
}
514514
s.pulumiCliSupportsViews = true
515515
return &pulumirpc.ProviderHandshakeResponse{
516-
AcceptSecrets: true,
517-
AcceptResources: true,
518-
AcceptOutputs: true,
516+
AcceptSecrets: true,
517+
AcceptResources: true,
518+
AcceptOutputs: true,
519+
SupportsAutonamingConfiguration: true,
519520
}, nil
520521
}
521522

tests/acc_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,35 @@ func Test_TwoInstances_TypeScript(t *testing.T) {
402402
})
403403
}
404404

405+
func TestAutomaticallySettingNameInputFromResourceName(t *testing.T) {
406+
t.Parallel()
407+
localProviderBinPath := ensureCompiledProvider(t)
408+
modulePath, err := filepath.Abs(filepath.Join("testdata", "modules", "autonaming"))
409+
require.NoError(t, err, "failed to get absolute path for autonaming module")
410+
program := filepath.Join("testdata", "programs", "yaml", "autonaming")
411+
integrationTest := pulumitest.NewPulumiTest(t, program,
412+
opttest.SkipInstall(),
413+
opttest.LocalProviderPath(provider, filepath.Dir(localProviderBinPath)))
414+
415+
packageName := "autonamed"
416+
pulumiPackageAdd(t, integrationTest, localProviderBinPath, modulePath, packageName)
417+
418+
result := integrationTest.Up(t)
419+
assert.Len(t, result.Outputs, 1, "expected one output")
420+
output, ok := result.Outputs["result"]
421+
assert.True(t, ok, "expected output called result")
422+
// the output should be the name of the resource, which is set to "exampleResourceName"
423+
resultValue, ok := output.Value.(string)
424+
assert.True(t, ok, "expected output value to be a string")
425+
assert.True(t, strings.HasPrefix(resultValue, "exampleResourceName-"))
426+
preview := integrationTest.Preview(t)
427+
assert.Equal(t, 2, preview.ChangeSummary[apitype.OpType("same")])
428+
429+
deleteResult := integrationTest.Destroy(t)
430+
expected := &map[string]int{"delete": 2}
431+
assert.Equal(t, deleteResult.Summary.ResourceChanges, expected, "should delete one resource")
432+
}
433+
405434
// Test that changing the source code of a local module causes the module to be updated
406435
// without any changes to the program code.
407436
// In this specific example, the change in the source code is from changing outputs

tests/examples_test.go

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ func Test_RdsExample(t *testing.T) {
6666
}
6767

6868
func Test_Azure_VirtualNetworkExample_NoExplicitProvider(t *testing.T) {
69-
t.Parallel()
70-
7169
if _, ci := os.LookupEnv("CI"); !ci {
7270
t.Skip("Skipping Azure tests in local runs without credentials.")
7371
}
@@ -88,17 +86,43 @@ func Test_Azure_VirtualNetworkExample_NoExplicitProvider(t *testing.T) {
8886
pulumiPackageAdd(t, integrationTest, localProviderBinPath,
8987
"Azure/avm-res-network-virtualnetwork/azurerm", "0.8.1", "vnet")
9088

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-
)
89+
// resource group name
90+
resourceGroupName := generateTestResourcePrefix()
91+
integrationTest.SetConfig(t, "rg", resourceGroupName+"-resource-group")
92+
93+
t.Run("up", func(t *testing.T) {
94+
// for debugging generated Terraform files during up
95+
terraformFiles := filepath.Join(testProgram, "up")
96+
t.Setenv("PULUMI_TERRAFORM_MODULE_WRITE_TF_FILE", terraformFiles)
97+
t.Cleanup(func() {
98+
cleanRandomDataFromTerraformArtifacts(t, terraformFiles, map[string]string{
99+
resourceGroupName: "RESOURCE_GROUP",
100+
})
101+
})
102+
103+
integrationTest.Up(t, optup.Diff(),
104+
optup.ErrorProgressStreams(tw),
105+
optup.ProgressStreams(tw),
106+
)
107+
})
108+
109+
t.Run("preview", func(t *testing.T) {
110+
// for debugging generated Terraform files during preview
111+
terraformFiles := filepath.Join(testProgram, "preview")
112+
t.Setenv("PULUMI_TERRAFORM_MODULE_WRITE_TF_FILE", terraformFiles)
113+
t.Cleanup(func() {
114+
cleanRandomDataFromTerraformArtifacts(t, terraformFiles, map[string]string{
115+
resourceGroupName: "RESOURCE_GROUP",
116+
})
117+
})
118+
119+
integrationTest.Preview(t,
120+
optpreview.Diff(),
121+
optpreview.ExpectNoChanges(),
122+
optpreview.ErrorProgressStreams(tw),
123+
optpreview.ProgressStreams(tw),
124+
)
125+
})
102126
}
103127

104128
func Test_EksExample(t *testing.T) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# example showing an input called name is automatically set
2+
variable "name" {
3+
type = string
4+
default = ""
5+
}
6+
7+
output "name" {
8+
value = var.name
9+
}

0 commit comments

Comments
 (0)