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:
parent
75744f1d6c
commit
fb1ebd0e06
|
@ -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 {
|
||||
|
|
3
examples/secrets/Pulumi.yaml
Normal file
3
examples/secrets/Pulumi.yaml
Normal 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
40
examples/secrets/index.ts
Normal 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;
|
9
examples/secrets/package.json
Normal file
9
examples/secrets/package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "secrets",
|
||||
"devDependencies": {
|
||||
"@types/node": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pulumi/pulumi": "latest"
|
||||
}
|
||||
}
|
34
examples/secrets/provider.ts
Normal file
34
examples/secrets/provider.ts
Normal 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);
|
||||
}
|
||||
}
|
22
examples/secrets/tsconfig.json
Normal file
22
examples/secrets/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