Add the ability to change secrets provider (#5031)
This commit is contained in:
parent
ce4badd488
commit
ed923a1057
|
@ -6,6 +6,9 @@ CHANGELOG
|
||||||
- Update pip/setuptools/wheel in virtual environment before installing dependencies
|
- Update pip/setuptools/wheel in virtual environment before installing dependencies
|
||||||
[#5042](https://github.com/pulumi/pulumi/pull/5042)
|
[#5042](https://github.com/pulumi/pulumi/pull/5042)
|
||||||
|
|
||||||
|
- Add ability to change a secrets provider for the current stack
|
||||||
|
[#5031](https://github.com/pulumi/pulumi/pull/5031)
|
||||||
|
|
||||||
## 2.7.1 (2020-07-22)
|
## 2.7.1 (2020-07-22)
|
||||||
|
|
||||||
- Fix logic to parse pulumi venv on github action
|
- Fix logic to parse pulumi venv on github action
|
||||||
|
|
|
@ -53,6 +53,12 @@ func getStackSecretsManager(s backend.Stack) (secrets.Manager, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sm, err := func() (secrets.Manager, error) {
|
sm, err := func() (secrets.Manager, error) {
|
||||||
|
if ps.SecretsProvider == "" {
|
||||||
|
switch s.(type) {
|
||||||
|
case httpstate.Stack:
|
||||||
|
return newServiceSecretsManager(s.(httpstate.Stack), s.Ref().Name(), stackConfigFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
if ps.SecretsProvider != passphrase.Type && ps.SecretsProvider != "default" && ps.SecretsProvider != "" {
|
if ps.SecretsProvider != passphrase.Type && ps.SecretsProvider != "default" && ps.SecretsProvider != "" {
|
||||||
return newCloudSecretsManager(s.Ref().Name(), stackConfigFile, ps.SecretsProvider)
|
return newCloudSecretsManager(s.Ref().Name(), stackConfigFile, ps.SecretsProvider)
|
||||||
}
|
}
|
||||||
|
@ -61,9 +67,7 @@ func getStackSecretsManager(s backend.Stack) (secrets.Manager, error) {
|
||||||
return newPassphraseSecretsManager(s.Ref().Name(), stackConfigFile)
|
return newPassphraseSecretsManager(s.Ref().Name(), stackConfigFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch stack := s.(type) {
|
switch s.(type) {
|
||||||
case httpstate.Stack:
|
|
||||||
return newServiceSecretsManager(stack)
|
|
||||||
case filestate.Stack:
|
case filestate.Stack:
|
||||||
return newPassphraseSecretsManager(s.Ref().Name(), stackConfigFile)
|
return newPassphraseSecretsManager(s.Ref().Name(), stackConfigFile)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,38 @@ import (
|
||||||
"github.com/pulumi/pulumi/pkg/v2/backend/httpstate"
|
"github.com/pulumi/pulumi/pkg/v2/backend/httpstate"
|
||||||
"github.com/pulumi/pulumi/pkg/v2/secrets"
|
"github.com/pulumi/pulumi/pkg/v2/secrets"
|
||||||
"github.com/pulumi/pulumi/pkg/v2/secrets/service"
|
"github.com/pulumi/pulumi/pkg/v2/secrets/service"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newServiceSecretsManager(s httpstate.Stack) (secrets.Manager, error) {
|
func newServiceSecretsManager(s httpstate.Stack, stackName tokens.QName, configFile string) (secrets.Manager, error) {
|
||||||
|
contract.Assertf(stackName != "", "stackName %s", "!= \"\"")
|
||||||
|
|
||||||
|
if configFile == "" {
|
||||||
|
f, err := workspace.DetectProjectStackPath(stackName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
configFile = f
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := workspace.LoadProjectStack(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
client := s.Backend().(httpstate.Backend).Client()
|
client := s.Backend().(httpstate.Backend).Client()
|
||||||
id := s.StackIdentifier()
|
id := s.StackIdentifier()
|
||||||
|
|
||||||
|
// Let's ensure these are empty as they should be for a service secrets manager
|
||||||
|
info.SecretsProvider = ""
|
||||||
|
info.EncryptedKey = ""
|
||||||
|
info.EncryptionSalt = ""
|
||||||
|
|
||||||
|
if err := workspace.SaveProjectStack(stackName, info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return service.NewServiceSecretsManager(client, id)
|
return service.NewServiceSecretsManager(client, id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -179,6 +179,7 @@ func newStackCmd() *cobra.Command {
|
||||||
cmd.AddCommand(newStackSelectCmd())
|
cmd.AddCommand(newStackSelectCmd())
|
||||||
cmd.AddCommand(newStackTagCmd())
|
cmd.AddCommand(newStackTagCmd())
|
||||||
cmd.AddCommand(newStackRenameCmd())
|
cmd.AddCommand(newStackRenameCmd())
|
||||||
|
cmd.AddCommand(newStackChangeSecretsProviderCmd())
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
177
pkg/cmd/pulumi/stack_change_secrets_provider.go
Normal file
177
pkg/cmd/pulumi/stack_change_secrets_provider.go
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
// Copyright 2016-2020, Pulumi Corporation.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pulumi/pulumi/pkg/v2/backend"
|
||||||
|
"github.com/pulumi/pulumi/pkg/v2/resource/stack"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v2/go/common/apitype"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v2/go/common/resource/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/pulumi/pulumi/pkg/v2/backend/display"
|
||||||
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/cmdutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStackChangeSecretsProviderCmd() *cobra.Command {
|
||||||
|
var cmd = &cobra.Command{
|
||||||
|
Use: "change-secrets-provider <new-secrets-provider>",
|
||||||
|
Args: cmdutil.ExactArgs(1),
|
||||||
|
Short: "Change the secrets provider for the current stack",
|
||||||
|
Long: "Change the secrets provider for the current stack. " +
|
||||||
|
"Valid secret providers types are `default`, `passphrase`, `awskms`, `azurekeyvault`, `gcpkms`, `hashivault`.\n\n" +
|
||||||
|
"To change to using the Pulumi Default Secrets Provider, use the following:\n" +
|
||||||
|
"\n" +
|
||||||
|
"pulumi stack change-secrets-provider default" +
|
||||||
|
"\n" +
|
||||||
|
"\n" +
|
||||||
|
"To change the stack to use a cloud secrets backend, use one of the following:\n" +
|
||||||
|
"\n" +
|
||||||
|
"* `pulumi stack change-secrets-provider \"awskms://alias/ExampleAlias?region=us-east-1\"" +
|
||||||
|
"`\n" +
|
||||||
|
"* `pulumi stack change-secrets-provider " +
|
||||||
|
"\"awskms://1234abcd-12ab-34cd-56ef-1234567890ab?region=us-east-1\"`\n" +
|
||||||
|
"* `pulumi stack change-secrets-provider " +
|
||||||
|
"\"azurekeyvault://mykeyvaultname.vault.azure.net/keys/mykeyname\"`\n" +
|
||||||
|
"* `pulumi stack change-secrets-provider " +
|
||||||
|
"\"gcpkms://projects/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k>\"`\n" +
|
||||||
|
"* `pulumi stack change-secrets-provider \"hashivault://mykey\"`",
|
||||||
|
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||||
|
opts := display.Options{
|
||||||
|
Color: cmdutil.GetGlobalColorization(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate secrets provider type
|
||||||
|
if err := validateSecretsProvider(args[0]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current backend
|
||||||
|
b, err := currentBackend(opts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current stack and its project
|
||||||
|
currentStack, err := requireStack("", false, opts, true /*setCurrent*/)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
currentProjectStack, err := loadProjectStack(currentStack)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build decrypter based on the existing secrets provider
|
||||||
|
var decrypter config.Decrypter
|
||||||
|
currentConfig := currentProjectStack.Config
|
||||||
|
|
||||||
|
if currentConfig.HasSecureValue() {
|
||||||
|
dec, decerr := getStackDecrypter(currentStack)
|
||||||
|
if decerr != nil {
|
||||||
|
return decerr
|
||||||
|
}
|
||||||
|
decrypter = dec
|
||||||
|
} else {
|
||||||
|
decrypter = config.NewPanicCrypter()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new secrets provider and set to the currentStack
|
||||||
|
if err := createSecretsManager(b, currentStack.Ref(), args[0]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the config to use the new secrets provider
|
||||||
|
err = migrateConfigToNewSecretsProvider(currentStack, currentConfig, decrypter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixup the checkpoint
|
||||||
|
return migrateCheckpointToNewSecretsProvider(commandContext(), currentStack)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateCheckpointToNewSecretsProvider(ctx context.Context, currentStack backend.Stack) error {
|
||||||
|
// Load the current checkpoint so those secrets can also be decrypted
|
||||||
|
checkpoint, err := currentStack.ExportDeployment(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
snap, err := stack.DeserializeUntypedDeployment(checkpoint, stack.DefaultSecretsProvider)
|
||||||
|
if err != nil {
|
||||||
|
return checkDeploymentVersionError(err, currentStack.Ref().Name().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the newly created secrets manager for the stack
|
||||||
|
newSecretsManager, err := getStackSecretsManager(currentStack)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserialize the Snapshopshot with the NewSecrets Manager
|
||||||
|
reserializedDeployment, err := stack.SerializeDeployment(snap, newSecretsManager, false /*showSecrets*/)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := json.Marshal(reserializedDeployment)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dep := apitype.UntypedDeployment{
|
||||||
|
Version: apitype.DeploymentSchemaVersionCurrent,
|
||||||
|
Deployment: bytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the newly changes Deployment
|
||||||
|
return currentStack.ImportDeployment(ctx, &dep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateConfigToNewSecretsProvider(currentStack backend.Stack, currentConfig config.Map,
|
||||||
|
decrypter config.Decrypter) error {
|
||||||
|
// Get the new encrypter for the current stack
|
||||||
|
newEncrypter, err := getStackEncrypter(currentStack)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a copy of the current config map and re-encrypt using the new secrets provider
|
||||||
|
newProjectConfig, err := currentConfig.Copy(decrypter, newEncrypter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the project stack after the new secretsProvider is in place
|
||||||
|
reloadedProjectStack, err := loadProjectStack(currentStack)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val := range newProjectConfig {
|
||||||
|
if err := reloadedProjectStack.Config.Set(key, val, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveProjectStack(currentStack, reloadedProjectStack)
|
||||||
|
}
|
|
@ -114,11 +114,7 @@ func commandContext() context.Context {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
// createStack creates a stack with the given name, and optionally selects it as the current.
|
func createSecretsManager(b backend.Backend, stackRef backend.StackReference, secretsProvider string) error {
|
||||||
func createStack(
|
|
||||||
b backend.Backend, stackRef backend.StackReference, opts interface{}, setCurrent bool,
|
|
||||||
secretsProvider string) (backend.Stack, error) {
|
|
||||||
|
|
||||||
// As part of creating the stack, we also need to configure the secrets provider for the stack.
|
// As part of creating the stack, we also need to configure the secrets provider for the stack.
|
||||||
// We need to do this configuration step for cases where we will be using with the passphrase
|
// We need to do this configuration step for cases where we will be using with the passphrase
|
||||||
// secrets provider or one of the cloud-backed secrets providers. We do not need to do this
|
// secrets provider or one of the cloud-backed secrets providers. We do not need to do this
|
||||||
|
@ -128,9 +124,27 @@ func createStack(
|
||||||
// The default when using the filestate backend is the passphrase secrets provider
|
// The default when using the filestate backend is the passphrase secrets provider
|
||||||
secretsProvider = passphrase.Type
|
secretsProvider = passphrase.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := b.(httpstate.Backend); ok && isDefaultSecretsProvider {
|
||||||
|
stack, err := state.CurrentStack(commandContext(), b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stack == nil {
|
||||||
|
// This means this is the first time we are initiating a stack
|
||||||
|
// there is no way a stack will exist here so we need to just return nil
|
||||||
|
// this will mean the "old" default behaviour will work for us
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, serviceSecretsErr := newServiceSecretsManager(stack.(httpstate.Stack),
|
||||||
|
stackRef.Name(), stackConfigFile); serviceSecretsErr != nil {
|
||||||
|
return serviceSecretsErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if secretsProvider == passphrase.Type {
|
if secretsProvider == passphrase.Type {
|
||||||
if _, pharseErr := newPassphraseSecretsManager(stackRef.Name(), stackConfigFile); pharseErr != nil {
|
if _, pharseErr := newPassphraseSecretsManager(stackRef.Name(), stackConfigFile); pharseErr != nil {
|
||||||
return nil, pharseErr
|
return pharseErr
|
||||||
}
|
}
|
||||||
} else if !isDefaultSecretsProvider {
|
} else if !isDefaultSecretsProvider {
|
||||||
// All other non-default secrets providers are handled by the cloud secrets provider which
|
// All other non-default secrets providers are handled by the cloud secrets provider which
|
||||||
|
@ -141,7 +155,7 @@ func createStack(
|
||||||
if strings.HasPrefix(secretsProvider, "azurekeyvault://") {
|
if strings.HasPrefix(secretsProvider, "azurekeyvault://") {
|
||||||
parsed, err := url.Parse(secretsProvider)
|
parsed, err := url.Parse(secretsProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to parse secrets provider URL")
|
return errors.Wrap(err, "failed to parse secrets provider URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsed.Query().Get("algorithm") == "" {
|
if parsed.Query().Get("algorithm") == "" {
|
||||||
|
@ -151,10 +165,18 @@ func createStack(
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, secretsErr := newCloudSecretsManager(stackRef.Name(), stackConfigFile, secretsProvider); secretsErr != nil {
|
if _, secretsErr := newCloudSecretsManager(stackRef.Name(), stackConfigFile, secretsProvider); secretsErr != nil {
|
||||||
return nil, secretsErr
|
return secretsErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createStack creates a stack with the given name, and optionally selects it as the current.
|
||||||
|
func createStack(
|
||||||
|
b backend.Backend, stackRef backend.StackReference, opts interface{}, setCurrent bool,
|
||||||
|
secretsProvider string) (backend.Stack, error) {
|
||||||
|
|
||||||
stack, err := b.CreateStack(commandContext(), stackRef, opts)
|
stack, err := b.CreateStack(commandContext(), stackRef, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If it's a well-known error, don't wrap it.
|
// If it's a well-known error, don't wrap it.
|
||||||
|
@ -167,6 +189,10 @@ func createStack(
|
||||||
return nil, errors.Wrapf(err, "could not create stack")
|
return nil, errors.Wrapf(err, "could not create stack")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := createSecretsManager(b, stackRef, secretsProvider); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if setCurrent {
|
if setCurrent {
|
||||||
if err = state.SetCurrentStack(stack.Ref().String()); err != nil {
|
if err = state.SetCurrentStack(stack.Ref().String()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
Loading…
Reference in a new issue