Correctly flow secretness across POJO serliazation for stack outputs

Our logic to export a resource as a stack output transforms the
resource into a plain old object by eliding internal fields and then
just serializing the resource as a POJO.

The custom serialization logic we used here unwrapped an Output
without care to see if it held a secret. Now, when it does, we
continue to return an Output as the thing to be serialized and that
output is marked as a secret.

Fixes #2862
This commit is contained in:
Matt Ellis 2019-06-24 16:45:54 -07:00
parent c47ae413c3
commit 881db4d72a
8 changed files with 109 additions and 2 deletions

View file

@ -5,6 +5,10 @@ CHANGELOG
- Python SDK fix for a crash resulting from a KeyError if secrets were used in configuration.
- Fix an issue where a secret would not be encrypted in the state file if it was
a property of a resource which was used as a stack output (fixes
[#2862](https://github.com/pulumi/pulumi/issues/2862))
## 0.17.20 (2019-06-23)
- SDK fix for crash that could occasionally happen if there were multiple identical aliases to the

View file

@ -14,7 +14,7 @@
import * as asset from "../asset";
import { getProject, getStack } from "../metadata";
import { Inputs, Output, output } from "../output";
import { Inputs, Output, output, secret } from "../output";
import { ComponentResource, Resource } from "../resource";
import { getRootResource, isQueryMode, setRootResource } from "./settings";
@ -104,7 +104,17 @@ async function massage(prop: any, seenObjects: Set<any>): Promise<any> {
}
if (Output.isInstance(prop)) {
return await massage(await prop.promise(), seenObjects);
// If the output itself is a secret, we don't want to lose the secretness by returning the underlying
// value. So instead, we massage the underlying value and then wrap it back up in an Output which is
// marked as secret.
const isSecret = await (prop.isSecret || Promise.resolve(false));
const value = await massage(await prop.promise(), seenObjects);
if (isSecret) {
return secret(value);
}
return value;
}
// from this point on, we have complex objects. If we see them again, we don't want to emit

View file

@ -960,3 +960,30 @@ func TestProviderSecretConfig(t *testing.T) {
Quick: true,
})
}
func TestResourceWithSecretSerialization(t *testing.T) {
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: "secret_outputs",
Dependencies: []string{"@pulumi/pulumi"},
Quick: true,
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
// The program exports two resources, one named `withSecret` who's prefix property should be secret
// and one named `withoutSecret` which should not. We serialize both of the these as POJO objects, so
// they appear as maps in the output.
withSecretProps, ok := stackInfo.Outputs["withSecret"].(map[string]interface{})
assert.Truef(t, ok, "POJO output was not serialized as a map")
withoutSecretProps, ok := stackInfo.Outputs["withoutSecret"].(map[string]interface{})
assert.Truef(t, ok, "POJO output was not serialized as a map")
// The secret prop should have been serialized as a secret
secretPropValue, ok := withSecretProps["prefix"].(map[string]interface{})
assert.Truef(t, ok, "secret output was not serialized as a secret")
assert.Equal(t, resource.SecretSig, secretPropValue[resource.SigKey].(string))
// And here, the prop was not set, it should just be a string value
_, isString := withoutSecretProps["prefix"].(string)
assert.Truef(t, isString, "non-secret output was not a string")
},
})
}

View file

@ -0,0 +1,3 @@
name: secret-outputs
runtime: nodejs
description: A minimal TypeScript Pulumi program

View file

@ -0,0 +1,10 @@
import * as pulumi from "@pulumi/pulumi";
import { R } from "./res";
export const withoutSecret = new R("withoutSecret", {
prefix: pulumi.output("it's a secret to everybody")
});
export const withSecret = new R("withSecret", {
prefix: pulumi.secret("it's a secret to everybody")
});

View file

@ -0,0 +1,9 @@
{
"name": "typescript",
"devDependencies": {
"@types/node": "latest"
},
"peerDependencies": {
"@pulumi/pulumi": "latest"
}
}

View file

@ -0,0 +1,22 @@
import * as pulumi from "@pulumi/pulumi";
import * as dynamic from "@pulumi/pulumi/dynamic";
export interface RArgs {
prefix: pulumi.Input<string>
}
const provider: pulumi.dynamic.ResourceProvider = {
async create(inputs) {
return { id: "1", outs: {
prefix: inputs["prefix"]
}};
}
}
export class R extends dynamic.Resource {
public prefix: pulumi.Output<string>;
constructor(name: string, props: RArgs, opts?: pulumi.CustomResourceOptions) {
super(provider, name, props, opts)
}
}

View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"lib": [
"es6"
],
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true
},
"files": [
"index.ts"
]
}