pulumi / pulumi

Pulumi - Infrastructure as Code in any programming language 🚀
https://www.pulumi.com
Apache License 2.0
21.94k stars 1.13k forks source link

Output property missing during initial preview when the input includes a nested unknown value #7854

Open justinvp opened 3 years ago

justinvp commented 3 years ago

To reproduce, in a new empty directory run pulumi new aws-typescript.

Update index.ts with the following:

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// Use this resource's ARN for an unknown value.
const key = new aws.kms.Key("a");

// This bucket has no unknown values in its tags object; the tags are available in the policy pack at preview time as
// expected.
new aws.s3.Bucket("bucket-without-unknowns", {
    tags: {
        promptValue: "promptValue",
    }
});

// This bucket has an unknown value in its tags object. We would expect that the 'foo' tag value would be available at
// preview time, because it's provided as a prompt value; however, the entire object is treated as an unknown at preview
new aws.s3.Bucket("bucket-with-nested-unknown", {
    tags: {
        promptValue: "promptValue",
        unknown: key.arn
    }
});

Create a policy directory inside the project dir, cd into that directory and run pulumi policy new aws-typescript.

Update index.ts with the following:

import * as aws from "@pulumi/aws";
import {PolicyPack, ReportViolation, StackValidationArgs} from "@pulumi/policy";

new PolicyPack("aws-typescript", {
    policies: [
        {
            name: "print-s3-props",
            description: "Print the props of S3 bucket policy resources",
            enforcementLevel: "mandatory",
            validateStack: async (args: StackValidationArgs, reportViolation: ReportViolation): Promise<void> => {
                args.resources.forEach((policyResource => {
                    if (policyResource.isType(aws.s3.Bucket)) {
                        console.log("Resource Name:", policyResource.name)
                        console.log("Resource Props:", policyResource.props)
                    }
                }))
            },
        },
    ],
});

In this example, there are two buckets, each of which has a tags argument. Each of the buckets has one tag that is a prompt value. One of the buckets has a tag with an unknown value and one of them does not. There is a policy pack that prints out the props of each policy resource. It is expected that the tags prop would be available on both buckets. However, the tags object containing an unknown value is not available at preview time.

Change back into the project directory and run:

pulumi preview --policy-pack ./policy

This results in:

Previewing update (dev)

     Type                 Name                        Plan       Info
 +   pulumi:pulumi:Stack  obj-previews-dev            create     16 messages
 +   ├─ aws:kms:Key       a                           create
 +   ├─ aws:s3:Bucket     bucket-without-unknowns     create
 +   └─ aws:s3:Bucket     bucket-with-nested-unknown  create

Diagnostics:
  pulumi:pulumi:Stack ((obj-previews-dev):
    Resource Name: bucket-without-unknowns
    Resource Props: {
      acl: 'private',
      bucket: 'bucket-without-unknowns-1690972',
      forceDestroy: false,
      id: '',
      tags: { promptValue: 'promptValue' },
      tagsAll: { promptValue: 'promptValue' }
    }
    Resource Name: bucket-with-nested-unknown
    Resource Props: {
      acl: 'private',
      bucket: 'bucket-with-nested-unknown-e3a8e08',
      forceDestroy: false,
      id: ''
    }

In summary, if an object input property contains an unknown value, the property is missing from the outputs, even if some of the keys in the object are provided as prompt values.

justinvp commented 3 years ago

From the logs, these are the input properties passed to the AWS provider for bucket-with-nested-unknown:

Provider[aws].Create(bucket-with-nested-unknown) executing (#props=5)
...
Marshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).inputs]: __defaults={[{acl} {bucket} {forceDestroy}]}
Marshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).inputs]: acl={private}
Marshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).inputs]: bucket={bucket-with-nested-unknown-be14d40}
Marshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).inputs]: forceDestroy={false}
Marshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).inputs]: tags={map[__defaults:{[]} promptValue:{promptValue} unknown:output<string>{}]}

And these are the output properties that come back from the AWS provider (the tags property is missing):

Unmarshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).outputs]: acl={private}
Unmarshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).outputs]: bucket={bucket-with-nested-unknown-be14d40}
Unmarshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).outputs]: forceDestroy={false}
Unmarshaling property for RPC[Provider[aws].Create(bucket-with-nested-unknown).outputs]: id={}
Provider[aws].Create(bucket-with-nested-unknown) success: id=; #outs=4

This is happening in the TF bridge, and possibly the TF provider. We need to understand if this is the TF behavior, if the behavior has changed at all, and whether we have any control over it.

From a Pulumi program perspective, this likely isn't too impactful. If the output property value doesn't exist, during preview, the Output instance will be marked as unknown, and during preview you wouldn't be able to interogate the nested values in the apply, since it won't be run.

But it is more significant for policies.

Possible Workaround

The policy in the repro above is a stack validation policy, which notably looks at each resource's output properties.

Resource validation policies, on the other hand, look at a single resource's input properties, and tags is present in the inputs.

Consider an updated ./policy/index.ts that includes a resource validation policy:

import * as aws from "@pulumi/aws";
import {PolicyPack, ReportViolation, ResourceValidationArgs, StackValidationArgs} from "@pulumi/policy";

new PolicyPack("aws-typescript", {
    policies: [
        {
            name: "bucket-resource-prop",
            description: "Print the props of S3 bucket resource.",
            enforcementLevel: "mandatory",
            validateResource: (args: ResourceValidationArgs, reportViolation: ReportViolation) => {
                if (args.isType(aws.s3.Bucket)) {
                    console.log("Resource: Resource Name:", args.name)
                    console.log("Resource: Resource Props:", args.props)
                }
            },
        },
        {
            name: "print-s3-props",
            description: "Print the props of S3 bucket policy resources",
            enforcementLevel: "mandatory",
            validateStack: async (args: StackValidationArgs, reportViolation: ReportViolation): Promise<void> => {
                args.resources.forEach((policyResource => {
                    if (policyResource.isType(aws.s3.Bucket)) {
                        console.log("Stack: Resource Name:", policyResource.name)
                        console.log("Stack: Resource Props:", policyResource.props)
                    }
                }))
            },
        },
    ],
});

Running pulumi preview --policy-pack ./policy has the following output:

    Resource: Resource Name: bucket-without-unknowns
    Resource: Resource Props: {
      acl: 'private',
      bucket: 'bucket-without-unknowns-4384ea1',
      forceDestroy: false,
      tags: { promptValue: 'promptValue' }
    }
    Resource: Resource Name: bucket-with-nested-unknown
    Resource: Resource Props: {
      acl: 'private',
      bucket: 'bucket-with-nested-unknown-5c498fc',
      forceDestroy: false,
      tags: {
        promptValue: 'promptValue',
        unknown: '04da6b54-80e4-46f7-96ec-b56ff0331ba9'
      }
    }
    Stack: Resource Name: bucket-without-unknowns
    Stack: Resource Props: {
      acl: 'private',
      bucket: 'bucket-without-unknowns-4384ea1',
      forceDestroy: false,
      id: '',
      tags: { promptValue: 'promptValue' },
      tagsAll: { promptValue: 'promptValue' }
    }
    Stack: Resource Name: bucket-with-nested-unknown
    Stack: Resource Props: {
      acl: 'private',
      bucket: 'bucket-with-nested-unknown-5c498fc',
      forceDestroy: false,
      id: ''
    }

One potential workaround would be to use a resource validation policy that maintains a global map of resource URNs to inputs, and then in the stack validation policy, look at a resource’s inputs rather than outputs from the map.


import * as aws from "@pulumi/aws";
import {PolicyPack, ReportViolation, ResourceValidationArgs, StackValidationArgs} from "@pulumi/policy";

const urnToInputsMap = new Map<string, Record<string, any>>();

new PolicyPack("aws-typescript", {
    policies: [
        {
            name: "urn-to-inputs",
            description: "Track resource inputs.",
            enforcementLevel: "mandatory",
            validateResource: (args: ResourceValidationArgs, reportViolation: ReportViolation) => {
                urnToInputsMap.set(args.urn, args.props);
            },
        },
        {
            name: "print-s3-props",
            description: "Print the props of S3 bucket policy resources",
            enforcementLevel: "mandatory",
            validateStack: async (args: StackValidationArgs, reportViolation: ReportViolation): Promise<void> => {
                args.resources.forEach((policyResource => {
                    if (policyResource.isType(aws.s3.Bucket)) {
                        console.log("Resource Name:", policyResource.name);
                        console.log("Resource Inputs:", urnToInputsMap.get(policyResource.urn));
                        console.log("Resource Outputs:", policyResource.props);
                    }
                }))
            },
        },
    ],
});

Which outputs:

    Resource Name: bucket-without-unknowns
    Resource Inputs: {
      acl: 'private',
      bucket: 'bucket-without-unknowns-81f4a30',
      forceDestroy: false,
      tags: { promptValue: 'promptValue' }
    }
    Resource Outputs: {
      acl: 'private',
      bucket: 'bucket-without-unknowns-81f4a30',
      forceDestroy: false,
      id: '',
      tags: { promptValue: 'promptValue' },
      tagsAll: { promptValue: 'promptValue' }
    }
    Resource Name: bucket-with-nested-unknown
    Resource Inputs: {
      acl: 'private',
      bucket: 'bucket-with-nested-unknown-3985369',
      forceDestroy: false,
      tags: {
        promptValue: 'promptValue',
        unknown: '04da6b54-80e4-46f7-96ec-b56ff0331ba9'
      }
    }
    Resource Outputs: {
      acl: 'private',
      bucket: 'bucket-with-nested-unknown-3985369',
      forceDestroy: false,
      id: ''
    }
t0yv0 commented 3 years ago

I was interested in this, would love to dig in sometime or hear what the root cause of the issue was after all.