Add an End to End test of secrets

This test ensures that our secrets support works by deploying
resources with a mix of secret and plantext inputs and then checks the
deployment file to ensure that everything is marked as a secret.
This commit is contained in:
Matt Ellis 2019-05-10 12:30:41 -07:00
parent 75744f1d6c
commit fb1ebd0e06
6 changed files with 176 additions and 1 deletions

View file

@ -12,11 +12,12 @@ import (
"github.com/blang/semver"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pulumi/pulumi/pkg/resource/deploy/providers"
"github.com/pulumi/pulumi/pkg/testing/integration"
"github.com/pulumi/pulumi/pkg/util/contract"
)
func TestExamples(t *testing.T) {
@ -102,6 +103,72 @@ func TestExamples(t *testing.T) {
}))
}
// Add a secrets example: This deploys a program that spins up a bunch of custom resources with different sets
// of secret inputs.
examples = append(examples, integration.ProgramTestOptions{
Dir: path.Join(cwd, "secrets"),
Dependencies: []string{"@pulumi/pulumi"},
Config: map[string]string{
"message": "plaintext message",
},
Secrets: map[string]string{
"apiKey": "FAKE_API_KEY_FOR_TESTING",
},
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
assert.NotNil(t, stackInfo.Deployment.SecretsProviders, "Deployment should have a secrets provider")
isEncrypted := func(v interface{}) bool {
if m, ok := v.(map[string]interface{}); ok {
sigKey := m[resource.SigKey]
if sigKey == nil {
return false
}
v, vOk := sigKey.(string)
if !vOk {
return false
}
if v != resource.SecretSig {
return false
}
ciphertext := m["ciphertext"]
if ciphertext == nil {
return false
}
_, cOk := ciphertext.(string)
return cOk
}
return false
}
for _, res := range stackInfo.Deployment.Resources {
if res.Type == "pulumi-nodejs:dynamic:Resource" {
switch res.URN.Name() {
case "sValue", "sApply", "cValue", "cApply":
assert.Truef(t, isEncrypted(res.Inputs["value"]), "input value should be encrypted")
assert.Truef(t, isEncrypted(res.Outputs["value"]), "output value should be encrypted")
case "pValue", "pApply":
assert.Falsef(t, isEncrypted(res.Inputs["value"]), "input value should not be encrypted")
assert.Falsef(t, isEncrypted(res.Outputs["value"]), "output value should not be encrypted")
case "pDummy":
assert.Falsef(t, isEncrypted(res.Outputs["value"]), "output value should not be encrypted")
case "sDummy":
// Creation of this resource passes in a custom resource options to ensure that "value" is
// treated as secret. In the state file, we'll see this as an uncrypted input with an
// encrypted output.
assert.Truef(t, isEncrypted(res.Outputs["value"]), "output value should be encrypted")
default:
contract.Assertf(false, "unknown name type: %s", res.URN.Name())
}
}
}
},
})
// The compat test only works on Node 6.10.X because its uses the old 0.10.0 pulumi package, which only supported
// a single node version, since it had the native runtime component.
if nodeVer, err := getNodeVersion(); err != nil && nodeVer.Major == 6 && nodeVer.Minor == 10 {

View file

@ -0,0 +1,3 @@
name: secrets-dynamic
runtime: nodejs
description: A small example that uses our secrets support.

40
examples/secrets/index.ts Normal file
View file

@ -0,0 +1,40 @@
import * as pulumi from "@pulumi/pulumi";
import { ReflectResource, DummyResource } from "./provider";
const c = new pulumi.Config();
// ApiKey is an Output<string> and marked as a secret. If it is used as an input for any resources, the value will
// be encrypted.
const apiKey = c.requireSecret("apiKey");
// A plaintext message. We could turn this into a secret after the fact by passing it to `pulumi.secret` if we wished.
const message = c.require("message");
// Secrets are viral. When you combine secrets with `pulumi.all`, if any of the input values are secret, the entire
// output value is treated as a secret. Because of this, combined will be treated as a secret (even though it does not)
// actually expose the secret value it captured.
const combined = pulumi.all([apiKey, message]).apply(([s, p]) => {
return p;
})
// Since these inputs are either directly secrets, or become secrets via an `apply` of a secret, we expect that in
// the state file, they will be encrypted.
export const secretMessage = new ReflectResource("sValue", apiKey).value;
export const secretApply = new ReflectResource("sApply", apiKey.apply(x => x.length)).value;
// These are paintext values, so they will be stored as is in the state file.
export const plaintextMessage = new ReflectResource("pValue", message).value;
export const plaintextApply = new ReflectResource("pApply", message.length).value;
// These are secrets, as well, based on the composition above. We expect that these will also be stored as secrets
// in the state file.
export const combinedMessage = new ReflectResource("cValue", combined).value;
export const combinedApply = new ReflectResource("cApply", combined.apply(x => x.length)).value;
// The dummy resource just provides a single output named "value" with a simple message. But we can use
// `additionalSecretOutputs` as a way to enforce that it is treated as a secret.
export const dummyValue = new DummyResource("pDummy").value;
export const dummyValueAdditionalSecrets = new DummyResource("sDummy", {
additionalSecretOutputs: ["value"],
}).value;

View file

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

View file

@ -0,0 +1,34 @@
import * as pulumi from "@pulumi/pulumi";
import * as dynamic from "@pulumi/pulumi/dynamic";
class ReflectProvider implements dynamic.ResourceProvider {
public check(olds: any, news: any) { return Promise.resolve({ inputs: news }); }
public diff(id: pulumi.ID, olds: any, news: any) { return Promise.resolve({}); }
public delete(id: pulumi.ID, props: any) { return Promise.resolve(); }
public create(inputs: any) { return Promise.resolve({ id: "0", outs: inputs }); }
public update(id: string, olds: any, news: any) { return Promise.resolve({ outs: news }); }
}
export class ReflectResource extends dynamic.Resource {
public readonly value: pulumi.Output<string>;
constructor(name: string, value: pulumi.Input<any>, opts?: pulumi.CustomResourceOptions) {
super(new ReflectProvider(), name, {value: value }, opts);
}
}
class DummyProvider implements dynamic.ResourceProvider {
public check(olds: any, news: any) { return Promise.resolve({ inputs: news }); }
public diff(id: pulumi.ID, olds: any, news: any) { return Promise.resolve({}); }
public delete(id: pulumi.ID, props: any) { return Promise.resolve(); }
public create(inputs: any) { return Promise.resolve({ id: "0", outs: {"value": "hello"} }); }
public update(id: string, olds: any, news: any) { return Promise.resolve({ outs: news }); }
}
export class DummyResource extends dynamic.Resource {
public readonly value: pulumi.Output<string>;
constructor(name: string, opts?: pulumi.CustomResourceOptions) {
super(new DummyProvider(), name, {}, 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"
]
}