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:
parent
c47ae413c3
commit
881db4d72a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
3
tests/integration/secret_outputs/Pulumi.yaml
Normal file
3
tests/integration/secret_outputs/Pulumi.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
name: secret-outputs
|
||||
runtime: nodejs
|
||||
description: A minimal TypeScript Pulumi program
|
10
tests/integration/secret_outputs/index.ts
Normal file
10
tests/integration/secret_outputs/index.ts
Normal 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")
|
||||
});
|
9
tests/integration/secret_outputs/package.json
Normal file
9
tests/integration/secret_outputs/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "typescript",
|
||||
"devDependencies": {
|
||||
"@types/node": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pulumi/pulumi": "latest"
|
||||
}
|
||||
}
|
22
tests/integration/secret_outputs/res.ts
Normal file
22
tests/integration/secret_outputs/res.ts
Normal 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)
|
||||
}
|
||||
}
|
22
tests/integration/secret_outputs/tsconfig.json
Normal file
22
tests/integration/secret_outputs/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue