Add a --config-file option for stack ops (#2258)
This option allows the user to override the file used to fetch and store configuration information for a stack. It is available for the config, destroy, logs, preview, refresh, and up commands. Note that this option is not persistent: if it is not specified, the stack's default configuration will be used. If an alternate config file is used exclusively for a stack, it must be specified to all commands that interact with that stack. This option can be used to share plaintext configuration across multiple stacks. It cannot be used to share secret configuration, as secrets are associated with a particular stack and cannot be decryptex by other stacks.
This commit is contained in:
parent
55bea65276
commit
9c5526e7dd
|
@ -1,3 +1,9 @@
|
|||
## 0.16.7 (not yet released)
|
||||
|
||||
### Improvements
|
||||
|
||||
- Configuration and stack commands now take a `--config-file` options. This option allows the user to override the file used to fetch and store config information for a stack during the execution of a command.
|
||||
|
||||
|
||||
## 0.16.6 (Released November 28th, 2018)
|
||||
|
||||
|
|
|
@ -67,6 +67,9 @@ func newConfigCmd() *cobra.Command {
|
|||
cmd.PersistentFlags().StringVarP(
|
||||
&stack, "stack", "s", "",
|
||||
"The name of the stack to operate on. Defaults to the current stack")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&stackConfigFile, "config-file", "",
|
||||
"Use the configuration values in the specified file rather than detecting the file name")
|
||||
|
||||
cmd.AddCommand(newConfigGetCmd(&stack))
|
||||
cmd.AddCommand(newConfigRmCmd(&stack))
|
||||
|
@ -117,14 +120,13 @@ func newConfigRmCmd(stack *string) *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stackName := s.Ref().Name()
|
||||
|
||||
key, err := parseConfigKey(args[0])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid configuration key")
|
||||
}
|
||||
|
||||
ps, err := workspace.DetectProjectStack(stackName)
|
||||
ps, err := loadProjectStack(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -133,7 +135,7 @@ func newConfigRmCmd(stack *string) *cobra.Command {
|
|||
delete(ps.Config, key)
|
||||
}
|
||||
|
||||
return workspace.SaveProjectStack(stackName, ps)
|
||||
return saveProjectStack(s, ps)
|
||||
}),
|
||||
}
|
||||
|
||||
|
@ -156,14 +158,13 @@ func newConfigRefreshCmd(stack *string) *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stackName := s.Ref().Name()
|
||||
|
||||
c, err := backend.GetLatestConfiguration(commandContext(), s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath, err := workspace.DetectProjectStackPath(stackName)
|
||||
configPath, err := getProjectStackPath(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -201,7 +202,7 @@ func newConfigRefreshCmd(stack *string) *cobra.Command {
|
|||
|
||||
err = ps.Save(configPath)
|
||||
if err == nil {
|
||||
fmt.Printf("refreshed configuration for stack '%s'\n", stackName)
|
||||
fmt.Printf("refreshed configuration for stack '%s'\n", s.Ref().Name())
|
||||
}
|
||||
return err
|
||||
}),
|
||||
|
@ -233,7 +234,6 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stackName := s.Ref().Name()
|
||||
|
||||
key, err := parseConfigKey(args[0])
|
||||
if err != nil {
|
||||
|
@ -286,14 +286,14 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
ps, err := workspace.DetectProjectStack(stackName)
|
||||
ps, err := loadProjectStack(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ps.Config[key] = v
|
||||
|
||||
return workspace.SaveProjectStack(stackName, ps)
|
||||
return saveProjectStack(s, ps)
|
||||
}),
|
||||
}
|
||||
|
||||
|
@ -307,6 +307,29 @@ func newConfigSetCmd(stack *string) *cobra.Command {
|
|||
return setCmd
|
||||
}
|
||||
|
||||
var stackConfigFile string
|
||||
|
||||
func getProjectStackPath(stack backend.Stack) (string, error) {
|
||||
if stackConfigFile == "" {
|
||||
return workspace.DetectProjectStackPath(stack.Ref().Name())
|
||||
}
|
||||
return stackConfigFile, nil
|
||||
}
|
||||
|
||||
func loadProjectStack(stack backend.Stack) (*workspace.ProjectStack, error) {
|
||||
if stackConfigFile == "" {
|
||||
return workspace.DetectProjectStack(stack.Ref().Name())
|
||||
}
|
||||
return workspace.LoadProjectStack(stackConfigFile)
|
||||
}
|
||||
|
||||
func saveProjectStack(stack backend.Stack, ps *workspace.ProjectStack) error {
|
||||
if stackConfigFile == "" {
|
||||
return workspace.SaveProjectStack(stack.Ref().Name(), ps)
|
||||
}
|
||||
return ps.Save(stackConfigFile)
|
||||
}
|
||||
|
||||
func parseConfigKey(key string) (config.Key, error) {
|
||||
// As a convience, we'll treat any key with no delimiter as if:
|
||||
// <program-name>:<key> had been written instead
|
||||
|
@ -340,7 +363,7 @@ func prettyKeyForProject(k config.Key, proj *workspace.Project) string {
|
|||
}
|
||||
|
||||
func listConfig(stack backend.Stack, showSecrets bool) error {
|
||||
ps, err := workspace.DetectProjectStack(stack.Ref().Name())
|
||||
ps, err := loadProjectStack(stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -391,7 +414,7 @@ func listConfig(stack backend.Stack, showSecrets bool) error {
|
|||
}
|
||||
|
||||
func getConfig(stack backend.Stack, key config.Key) error {
|
||||
ps, err := workspace.DetectProjectStack(stack.Ref().Name())
|
||||
ps, err := loadProjectStack(stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -120,6 +120,9 @@ func newDestroyCmd() *cobra.Command {
|
|||
cmd.PersistentFlags().StringVarP(
|
||||
&stack, "stack", "s", "",
|
||||
"The name of the stack to operate on. Defaults to the current stack")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&stackConfigFile, "config-file", "",
|
||||
"Use the configuration values in the specified file rather than detecting the file name")
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&message, "message", "m", "",
|
||||
"Optional message to associate with the destroy operation")
|
||||
|
|
|
@ -87,9 +87,9 @@ func newLoginCmd() *cobra.Command {
|
|||
var be backend.Backend
|
||||
var err error
|
||||
if filestate.IsLocalBackendURL(cloudURL) {
|
||||
be, err = filestate.Login(cmdutil.Diag(), cloudURL)
|
||||
be, err = filestate.Login(cmdutil.Diag(), cloudURL, "")
|
||||
} else {
|
||||
be, err = httpstate.Login(commandContext(), cmdutil.Diag(), cloudURL, displayOptions)
|
||||
be, err = httpstate.Login(commandContext(), cmdutil.Diag(), cloudURL, "", displayOptions)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "problem logging in")
|
||||
|
|
|
@ -69,9 +69,9 @@ func newLogoutCmd() *cobra.Command {
|
|||
var be backend.Backend
|
||||
var err error
|
||||
if filestate.IsLocalBackendURL(cloudURL) {
|
||||
be, err = filestate.New(cmdutil.Diag(), cloudURL)
|
||||
be, err = filestate.New(cmdutil.Diag(), cloudURL, "")
|
||||
} else {
|
||||
be, err = httpstate.New(cmdutil.Diag(), cloudURL)
|
||||
be, err = httpstate.New(cmdutil.Diag(), cloudURL, "")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -144,6 +144,9 @@ func newLogsCmd() *cobra.Command {
|
|||
logsCmd.PersistentFlags().StringVarP(
|
||||
&stack, "stack", "s", "",
|
||||
"The name of the stack to operate on. Defaults to the current stack")
|
||||
logsCmd.PersistentFlags().StringVar(
|
||||
&stackConfigFile, "config-file", "",
|
||||
"Use the configuration values in the specified file rather than detecting the file name")
|
||||
logsCmd.PersistentFlags().BoolVarP(
|
||||
&jsonOut, "json", "j", false, "Emit outputs as JSON")
|
||||
logsCmd.PersistentFlags().BoolVarP(
|
||||
|
|
|
@ -443,8 +443,8 @@ func stackInit(b backend.Backend, stackName string, setCurrent bool) (backend.St
|
|||
}
|
||||
|
||||
// saveConfig saves the config for the stack.
|
||||
func saveConfig(stackName tokens.QName, c config.Map) error {
|
||||
ps, err := workspace.DetectProjectStack(stackName)
|
||||
func saveConfig(stack backend.Stack, c config.Map) error {
|
||||
ps, err := loadProjectStack(stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -453,7 +453,7 @@ func saveConfig(stackName tokens.QName, c config.Map) error {
|
|||
ps.Config[k] = v
|
||||
}
|
||||
|
||||
return workspace.SaveProjectStack(stackName, ps)
|
||||
return saveProjectStack(stack, ps)
|
||||
}
|
||||
|
||||
// installDependencies will install dependencies for the project, e.g. by running
|
||||
|
|
|
@ -96,7 +96,7 @@ func newPluginInstallCmd() *cobra.Command {
|
|||
// Target the cloud URL for downloads.
|
||||
var releases httpstate.Backend
|
||||
if len(installs) > 0 && file == "" {
|
||||
r, err := httpstate.New(cmdutil.Diag(), httpstate.ValueOrDefaultURL(cloudURL))
|
||||
r, err := httpstate.New(cmdutil.Diag(), httpstate.ValueOrDefaultURL(cloudURL), "")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating API client")
|
||||
}
|
||||
|
|
|
@ -117,6 +117,9 @@ func newPreviewCmd() *cobra.Command {
|
|||
cmd.PersistentFlags().StringVarP(
|
||||
&stack, "stack", "s", "",
|
||||
"The name of the stack to operate on. Defaults to the current stack")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&stackConfigFile, "config-file", "",
|
||||
"Use the configuration values in the specified file rather than detecting the file name")
|
||||
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&message, "message", "m", "",
|
||||
|
|
|
@ -128,6 +128,9 @@ func newRefreshCmd() *cobra.Command {
|
|||
cmd.PersistentFlags().StringVarP(
|
||||
&stack, "stack", "s", "",
|
||||
"The name of the stack to operate on. Defaults to the current stack")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&stackConfigFile, "config-file", "",
|
||||
"Use the configuration values in the specified file rather than detecting the file name")
|
||||
|
||||
cmd.PersistentFlags().StringVarP(
|
||||
&message, "message", "m", "",
|
||||
|
|
|
@ -74,7 +74,7 @@ func newUpCmd() *cobra.Command {
|
|||
return err
|
||||
}
|
||||
|
||||
if err = saveConfig(s.Ref().Name(), commandLineConfig); err != nil {
|
||||
if err = saveConfig(s, commandLineConfig); err != nil {
|
||||
return errors.Wrap(err, "saving config")
|
||||
}
|
||||
}
|
||||
|
@ -312,6 +312,9 @@ func newUpCmd() *cobra.Command {
|
|||
cmd.PersistentFlags().StringVarP(
|
||||
&stack, "stack", "s", "",
|
||||
"The name of the stack to operate on. Defaults to the current stack")
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&stackConfigFile, "config-file", "",
|
||||
"Use the configuration values in the specified file rather than detecting the file name")
|
||||
cmd.PersistentFlags().StringArrayVarP(
|
||||
&configArray, "config", "c", []string{},
|
||||
"Config to use during the update")
|
||||
|
@ -402,7 +405,7 @@ func handleConfig(
|
|||
|
||||
// Save the config.
|
||||
if c != nil {
|
||||
if err = saveConfig(s.Ref().Name(), c); err != nil {
|
||||
if err = saveConfig(s, c); err != nil {
|
||||
return errors.Wrap(err, "saving config")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,9 +60,9 @@ func currentBackend(opts display.Options) (backend.Backend, error) {
|
|||
return nil, err
|
||||
}
|
||||
if filestate.IsLocalBackendURL(creds.Current) {
|
||||
return filestate.New(cmdutil.Diag(), creds.Current)
|
||||
return filestate.New(cmdutil.Diag(), creds.Current, stackConfigFile)
|
||||
}
|
||||
return httpstate.Login(commandContext(), cmdutil.Diag(), creds.Current, opts)
|
||||
return httpstate.Login(commandContext(), cmdutil.Diag(), creds.Current, stackConfigFile, opts)
|
||||
}
|
||||
|
||||
// This is used to control the contents of the tracing header.
|
||||
|
|
|
@ -54,8 +54,9 @@ type Backend interface {
|
|||
}
|
||||
|
||||
type localBackend struct {
|
||||
d diag.Sink
|
||||
url string
|
||||
d diag.Sink
|
||||
url string
|
||||
stackConfigFile string
|
||||
}
|
||||
|
||||
type localBackendReference struct {
|
||||
|
@ -74,18 +75,19 @@ func IsLocalBackendURL(url string) bool {
|
|||
return strings.HasPrefix(url, localBackendURLPrefix)
|
||||
}
|
||||
|
||||
func New(d diag.Sink, url string) (Backend, error) {
|
||||
func New(d diag.Sink, url, stackConfigFile string) (Backend, error) {
|
||||
if !IsLocalBackendURL(url) {
|
||||
return nil, errors.Errorf("local URL %s has an illegal prefix; expected %s", url, localBackendURLPrefix)
|
||||
}
|
||||
return &localBackend{
|
||||
d: d,
|
||||
url: url,
|
||||
d: d,
|
||||
url: url,
|
||||
stackConfigFile: stackConfigFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Login(d diag.Sink, url string) (Backend, error) {
|
||||
be, err := New(d, url)
|
||||
func Login(d diag.Sink, url, stackConfigFile string) (Backend, error) {
|
||||
be, err := New(d, url, stackConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -213,7 +215,7 @@ func (b *localBackend) RemoveStack(ctx context.Context, stackRef backend.StackRe
|
|||
}
|
||||
|
||||
func (b *localBackend) GetStackCrypter(stackRef backend.StackReference) (config.Crypter, error) {
|
||||
return symmetricCrypter(stackRef.Name())
|
||||
return symmetricCrypter(stackRef.Name(), b.stackConfigFile)
|
||||
}
|
||||
|
||||
func (b *localBackend) GetLatestConfiguration(ctx context.Context,
|
||||
|
|
|
@ -38,21 +38,29 @@ func readPassphrase(prompt string) (string, error) {
|
|||
}
|
||||
|
||||
// defaultCrypter gets the right value encrypter/decrypter given the project configuration.
|
||||
func defaultCrypter(stackName tokens.QName, cfg config.Map) (config.Crypter, error) {
|
||||
func defaultCrypter(stackName tokens.QName, cfg config.Map, configFile string) (config.Crypter, error) {
|
||||
// If there is no config, we can use a standard panic crypter.
|
||||
if !cfg.HasSecureValue() {
|
||||
return config.NewPanicCrypter(), nil
|
||||
}
|
||||
|
||||
// Otherwise, we will use an encrypted one.
|
||||
return symmetricCrypter(stackName)
|
||||
return symmetricCrypter(stackName, configFile)
|
||||
}
|
||||
|
||||
// symmetricCrypter gets the right value encrypter/decrypter for this project.
|
||||
func symmetricCrypter(stackName tokens.QName) (config.Crypter, error) {
|
||||
func symmetricCrypter(stackName tokens.QName, configFile string) (config.Crypter, error) {
|
||||
contract.Assertf(stackName != "", "stackName %s", "!= \"\"")
|
||||
|
||||
info, err := workspace.DetectProjectStack(stackName)
|
||||
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
|
||||
}
|
||||
|
@ -98,7 +106,7 @@ func symmetricCrypter(stackName tokens.QName) (config.Crypter, error) {
|
|||
|
||||
// Now store the result and save it.
|
||||
info.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
|
||||
if err = workspace.SaveProjectStack(stackName, info); err != nil {
|
||||
if err = info.Save(configFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -86,11 +86,20 @@ func (b *localBackend) newUpdate(stackName tokens.QName, proj *workspace.Project
|
|||
}
|
||||
|
||||
func (b *localBackend) getTarget(stackName tokens.QName) (*deploy.Target, error) {
|
||||
stk, err := workspace.DetectProjectStack(stackName)
|
||||
stackConfigFile := b.stackConfigFile
|
||||
if stackConfigFile == "" {
|
||||
f, err := workspace.DetectProjectStackPath(stackName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stackConfigFile = f
|
||||
}
|
||||
|
||||
stk, err := workspace.LoadProjectStack(stackConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypter, err := defaultCrypter(stackName, stk.Config)
|
||||
decrypter, err := defaultCrypter(stackName, stk.Config, stackConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -139,13 +139,14 @@ type Backend interface {
|
|||
}
|
||||
|
||||
type cloudBackend struct {
|
||||
d diag.Sink
|
||||
url string
|
||||
client *client.Client
|
||||
d diag.Sink
|
||||
url string
|
||||
stackConfigFile string
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
// New creates a new Pulumi backend for the given cloud API URL and token.
|
||||
func New(d diag.Sink, cloudURL string) (Backend, error) {
|
||||
func New(d diag.Sink, cloudURL, stackConfigFile string) (Backend, error) {
|
||||
cloudURL = ValueOrDefaultURL(cloudURL)
|
||||
apiToken, err := workspace.GetAccessToken(cloudURL)
|
||||
if err != nil {
|
||||
|
@ -153,14 +154,15 @@ func New(d diag.Sink, cloudURL string) (Backend, error) {
|
|||
}
|
||||
|
||||
return &cloudBackend{
|
||||
d: d,
|
||||
url: cloudURL,
|
||||
client: client.NewClient(cloudURL, apiToken, d),
|
||||
d: d,
|
||||
url: cloudURL,
|
||||
stackConfigFile: stackConfigFile,
|
||||
client: client.NewClient(cloudURL, apiToken, d),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loginWithBrowser uses a web-browser to log into the cloud and returns the cloud backend for it.
|
||||
func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string) (Backend, error) {
|
||||
func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL, stackConfigFile string) (Backend, error) {
|
||||
// Locally, we generate a nonce and spin up a web server listening on a random port on localhost. We then open a
|
||||
// browser to a special endpoint on the Pulumi.com console, passing the generated nonce as well as the port of the
|
||||
// webserver we launched. This endpoint does the OAuth flow and when it completes, redirects to localhost passing
|
||||
|
@ -232,11 +234,11 @@ func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string) (Backen
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return New(d, cloudURL)
|
||||
return New(d, cloudURL, stackConfigFile)
|
||||
}
|
||||
|
||||
// Login logs into the target cloud URL and returns the cloud backend for it.
|
||||
func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error) {
|
||||
func Login(ctx context.Context, d diag.Sink, cloudURL, stackConfigFile string, opts display.Options) (Backend, error) {
|
||||
cloudURL = ValueOrDefaultURL(cloudURL)
|
||||
|
||||
// If we have a saved access token, and it is valid, use it.
|
||||
|
@ -248,7 +250,7 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return New(d, cloudURL)
|
||||
return New(d, cloudURL, stackConfigFile)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -315,7 +317,7 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
|
|||
}
|
||||
|
||||
if accessToken == "" {
|
||||
return loginWithBrowser(ctx, d, cloudURL)
|
||||
return loginWithBrowser(ctx, d, cloudURL, stackConfigFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -333,7 +335,7 @@ func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Optio
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return New(d, cloudURL)
|
||||
return New(d, cloudURL, stackConfigFile)
|
||||
}
|
||||
|
||||
func (b *cloudBackend) StackConsoleURL(stackRef backend.StackReference) (string, error) {
|
||||
|
@ -653,7 +655,15 @@ func (b *cloudBackend) createAndStartUpdate(
|
|||
if err != nil {
|
||||
return client.UpdateIdentifier{}, 0, "", err
|
||||
}
|
||||
workspaceStack, err := workspace.DetectProjectStack(stackRef.Name())
|
||||
stackConfigFile := b.stackConfigFile
|
||||
if stackConfigFile == "" {
|
||||
f, err := workspace.DetectProjectStackPath(stackRef.Name())
|
||||
if err != nil {
|
||||
return client.UpdateIdentifier{}, 0, "", err
|
||||
}
|
||||
stackConfigFile = f
|
||||
}
|
||||
workspaceStack, err := workspace.LoadProjectStack(stackConfigFile)
|
||||
if err != nil {
|
||||
return client.UpdateIdentifier{}, 0, "", errors.Wrap(err, "getting configuration")
|
||||
}
|
||||
|
|
|
@ -290,7 +290,15 @@ func (b *cloudBackend) getSnapshot(ctx context.Context, stackRef backend.StackRe
|
|||
|
||||
func (b *cloudBackend) getTarget(ctx context.Context, stackRef backend.StackReference) (*deploy.Target, error) {
|
||||
// Pull the local stack info so we can get at its configuration bag.
|
||||
stk, err := workspace.DetectProjectStack(stackRef.Name())
|
||||
stackConfigFile := b.stackConfigFile
|
||||
if stackConfigFile == "" {
|
||||
f, err := workspace.DetectProjectStackPath(stackRef.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stackConfigFile = f
|
||||
}
|
||||
stk, err := workspace.LoadProjectStack(stackConfigFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue