diff --git a/cmd/apply.go b/cmd/apply.go index ab0065b3c9433f735eb8a81a32b0d8d9e5bf8413..8462118070a322b5da71c51cf32fc9f132714c52 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -57,7 +57,7 @@ func init() { } var applyCmd = &cobra.Command{ - Use: "apply [<env>|-f <file-or-dir>]", + Use: "apply [env-name] [-f <file-or-dir>]", Short: `Apply local configuration to remote cluster`, RunE: func(cmd *cobra.Command, args []string) error { flags := cmd.Flags() @@ -85,27 +85,33 @@ var applyCmd = &cobra.Command{ return err } - c.ClientPool, c.Discovery, err = restClientPool(cmd) + cwd, err := os.Getwd() if err != nil { return err } + wd := metadata.AbsPath(cwd) - c.DefaultNamespace, _, err = clientConfig.Namespace() + envSpec, err := parseEnvCmd(cmd, args) if err != nil { return err } - cwd, err := os.Getwd() + c.ClientPool, c.Discovery, err = restClientPool(cmd, envSpec.env) if err != nil { return err } - objs, err := expandEnvCmdObjs(cmd, args) + c.DefaultNamespace, err = defaultNamespace(clientConfig) if err != nil { return err } - return c.Run(objs, metadata.AbsPath(cwd)) + objs, err := expandEnvCmdObjs(cmd, envSpec, wd) + if err != nil { + return err + } + + return c.Run(objs, wd) }, Long: `Update (or optionally create) Kubernetes resources on the cluster using the local configuration. Use the '--create' flag to control whether we create them @@ -122,6 +128,10 @@ files.`, # the cluster's location from '$KUBECONFIG'. ksonnet appy -f ./pod.yaml + # Create or update resources described in the JSON file. Changes are deployed + # to the cluster pointed at the 'dev' environment. + ksonnet apply dev -f ./pod.json + # Update resources described in a YAML file, and running in cluster referred # to by './kubeconfig'. ksonnet apply --kubeconfig=./kubeconfig -f ./pod.yaml diff --git a/cmd/delete.go b/cmd/delete.go index 5ebe5ba30d0cee04b0e713a04264e6b9d959bd2c..e0dc3f564beef3ef7502c8eca3bfab7eb9d3b4ff 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -16,8 +16,11 @@ package cmd import ( + "os" + "github.com/spf13/cobra" + "github.com/ksonnet/kubecfg/metadata" "github.com/ksonnet/kubecfg/pkg/kubecfg" ) @@ -32,7 +35,7 @@ func init() { } var deleteCmd = &cobra.Command{ - Use: "delete", + Use: "delete [env-name] [-f <file-or-dir>]", Short: "Delete Kubernetes resources described in local config", RunE: func(cmd *cobra.Command, args []string) error { flags := cmd.Flags() @@ -45,7 +48,18 @@ var deleteCmd = &cobra.Command{ return err } - c.ClientPool, c.Discovery, err = restClientPool(cmd) + cwd, err := os.Getwd() + if err != nil { + return err + } + wd := metadata.AbsPath(cwd) + + envSpec, err := parseEnvCmd(cmd, args) + if err != nil { + return err + } + + c.ClientPool, c.Discovery, err = restClientPool(cmd, envSpec.env) if err != nil { return err } @@ -55,7 +69,7 @@ var deleteCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, envSpec, wd) if err != nil { return err } diff --git a/cmd/diff.go b/cmd/diff.go index 887631a0ce73267dfc30938167bbfb9bce420353..bfcd7287cdb8f02266818c777d04c9e88e29a92d 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -16,8 +16,11 @@ package cmd import ( + "os" + "github.com/spf13/cobra" + "github.com/ksonnet/kubecfg/metadata" "github.com/ksonnet/kubecfg/pkg/kubecfg" ) @@ -30,7 +33,7 @@ func init() { } var diffCmd = &cobra.Command{ - Use: "diff [<env>|-f <file-or-dir>]", + Use: "diff [env-name] [-f <file-or-dir>]", Short: "Display differences between server and local config", RunE: func(cmd *cobra.Command, args []string) error { flags := cmd.Flags() @@ -43,7 +46,18 @@ var diffCmd = &cobra.Command{ return err } - c.ClientPool, c.Discovery, err = restClientPool(cmd) + cwd, err := os.Getwd() + if err != nil { + return err + } + wd := metadata.AbsPath(cwd) + + envSpec, err := parseEnvCmd(cmd, args) + if err != nil { + return err + } + + c.ClientPool, c.Discovery, err = restClientPool(cmd, envSpec.env) if err != nil { return err } @@ -53,7 +67,7 @@ var diffCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, envSpec, wd) if err != nil { return err } @@ -67,12 +81,16 @@ files.`, Example: ` # Show diff between resources described in a local ksonnet application and # the cluster referenced by the 'dev' environment. Can be used in any # subdirectory of the application. - ksonnet diff -e=dev + ksonnet diff dev # Show diff between resources described in a YAML file and the cluster # referenced in '$KUBECONFIG'. ksonnet diff -f ./pod.yaml + # Show diff between resources described in a JSON file and the cluster + # referenced by the environment 'dev'. + ksonnet diff dev -f ./pod.json + # Show diff between resources described in a YAML file and the cluster # referred to by './kubeconfig'. ksonnet diff --kubeconfig=./kubeconfig -f ./pod.yaml`, diff --git a/cmd/env.go b/cmd/env.go index adeec29bc8c4d62a12a609d0e7174a7928551623..68c3a8bcc962d7d8e6bb3be0113c62bb359cc108 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -101,12 +101,17 @@ var envAddCmd = &cobra.Command{ } appRoot := metadata.AbsPath(appDir) + manager, err := metadata.Find(appRoot) + if err != nil { + return err + } + specFlag, err := flags.GetString(flagAPISpec) if err != nil { return err } - c, err := kubecfg.NewEnvAddCmd(envName, envURI, specFlag, appRoot) + c, err := kubecfg.NewEnvAddCmd(envName, envURI, specFlag, manager) if err != nil { return err } @@ -168,7 +173,12 @@ var envRmCmd = &cobra.Command{ } appRoot := metadata.AbsPath(appDir) - c, err := kubecfg.NewEnvRmCmd(envName, appRoot) + manager, err := metadata.Find(appRoot) + if err != nil { + return err + } + + c, err := kubecfg.NewEnvRmCmd(envName, manager) if err != nil { return err } @@ -201,7 +211,12 @@ var envListCmd = &cobra.Command{ } appRoot := metadata.AbsPath(appDir) - c, err := kubecfg.NewEnvListCmd(appRoot) + manager, err := metadata.Find(appRoot) + if err != nil { + return err + } + + c, err := kubecfg.NewEnvListCmd(manager) if err != nil { return err } @@ -228,6 +243,11 @@ var envSetCmd = &cobra.Command{ } appRoot := metadata.AbsPath(appDir) + manager, err := metadata.Find(appRoot) + if err != nil { + return err + } + desiredEnvName, err := flags.GetString(flagEnvName) if err != nil { return err @@ -238,7 +258,7 @@ var envSetCmd = &cobra.Command{ return err } - c, err := kubecfg.NewEnvSetCmd(envName, desiredEnvName, desiredEnvURI, appRoot) + c, err := kubecfg.NewEnvSetCmd(envName, desiredEnvName, desiredEnvURI, manager) if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index 46e8d74d2c76f9956c8268291d49abf26aaf0f14..775c4795c06c0a7897f74586e432c9f01f00d4c3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ import ( "io" "os" "path/filepath" + "reflect" "strings" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -226,7 +227,14 @@ func dumpJSON(v interface{}) string { return string(buf.Bytes()) } -func restClientPool(cmd *cobra.Command) (dynamic.ClientPool, discovery.DiscoveryInterface, error) { +func restClientPool(cmd *cobra.Command, envName *string) (dynamic.ClientPool, discovery.DiscoveryInterface, error) { + if envName != nil { + err := overrideCluster(*envName) + if err != nil { + return nil, nil, err + } + } + conf, err := clientConfig.ClientConfig() if err != nil { return nil, nil, err @@ -245,6 +253,11 @@ func restClientPool(cmd *cobra.Command) (dynamic.ClientPool, discovery.Discovery return pool, discoCache, nil } +type envSpec struct { + env *string + files []string +} + // addEnvCmdFlags adds the flags that are common to the family of commands // whose form is `[<env>|-f <file-name>]`, e.g., `apply` and `delete`. func addEnvCmdFlags(cmd *cobra.Command) { @@ -253,12 +266,12 @@ func addEnvCmdFlags(cmd *cobra.Command) { // parseEnvCmd parses the family of commands that come in the form `[<env>|-f // <file-name>]`, e.g., `apply` and `delete`. -func parseEnvCmd(cmd *cobra.Command, args []string) (*string, []string, error) { +func parseEnvCmd(cmd *cobra.Command, args []string) (*envSpec, error) { flags := cmd.Flags() files, err := flags.GetStringArray(flagFile) if err != nil { - return nil, nil, err + return nil, err } var env *string @@ -266,25 +279,65 @@ func parseEnvCmd(cmd *cobra.Command, args []string) (*string, []string, error) { env = &args[0] } - return env, files, nil + return &envSpec{env: env, files: files}, nil } -// expandEnvCmdObjs finds and expands templates for the family of commands of -// the form `[<env>|-f <file-name>]`, e.g., `apply` and `delete`. That is, if -// the user passes a list of files, we will expand all templates in those files, -// while if a user passes an environment name, we will expand all component -// files using that environment. -func expandEnvCmdObjs(cmd *cobra.Command, args []string) ([]*unstructured.Unstructured, error) { - env, fileNames, err := parseEnvCmd(cmd, args) +// overrideCluster ensures that the cluster URI specified in the environment is +// associated in the user's kubeconfig file during deployment to a ksonnet +// environment. We will error out if it is not. +// +// If the environment URI the user is attempting to deploy to is not the current +// kubeconfig context, we must manually override the client-go --cluster flag +// to ensure we are deploying to the correct cluster. +func overrideCluster(envName string) error { + cwd, err := os.Getwd() if err != nil { - return nil, err + return err } + wd := metadata.AbsPath(cwd) - cwd, err := os.Getwd() + metadataManager, err := metadata.Find(wd) if err != nil { - return nil, err + return err } + rawConfig, err := clientConfig.RawConfig() + if err != nil { + return err + } + + var clusterURIs = make(map[string]string) + for name, cluster := range rawConfig.Clusters { + clusterURIs[cluster.Server] = name + } + + // + // check to ensure that the environment we are trying to deploy to is + // created, and that the environment URI is located in kubeconfig. + // + + log.Debugf("Validating deployment at '%s' with cluster URIs '%v'", envName, reflect.ValueOf(clusterURIs).MapKeys()) + env, err := metadataManager.GetEnvironment(envName) + if err != nil { + return err + } + + if _, ok := clusterURIs[env.URI]; ok { + clusterName := clusterURIs[env.URI] + log.Debugf("Overwriting --cluster flag with '%s'", clusterName) + overrides.Context.Cluster = clusterName + return nil + } + + return fmt.Errorf("Attempting to deploy to environment '%s' at %s, but there are no clusters with that URI", envName, env.URI) +} + +// expandEnvCmdObjs finds and expands templates for the family of commands of +// the form `[<env>|-f <file-name>]`, e.g., `apply` and `delete`. That is, if +// the user passes a list of files, we will expand all templates in those files, +// while if a user passes an environment name, we will expand all component +// files using that environment. +func expandEnvCmdObjs(cmd *cobra.Command, envSpec *envSpec, cwd metadata.AbsPath) ([]*unstructured.Unstructured, error) { expander, err := newExpander(cmd) if err != nil { return nil, err @@ -296,23 +349,21 @@ func expandEnvCmdObjs(cmd *cobra.Command, args []string) ([]*unstructured.Unstru // sure that the user either passed an environment name or a `-f` flag. // - envPresent := env != nil - filesPresent := len(fileNames) > 0 + envPresent := envSpec.env != nil + filesPresent := len(envSpec.files) > 0 - // This is equivalent to: `if !xor(envPresent, filesPresent) {` - if envPresent && filesPresent { - return nil, fmt.Errorf("Either an environment name or a file list is required, but not both") - } else if !envPresent && !filesPresent { - return nil, fmt.Errorf("Must specify either an environment or a file list") + if !envPresent && !filesPresent { + return nil, fmt.Errorf("Must specify either an environment or a file list, or both") } - if envPresent { - manager, err := metadata.Find(metadata.AbsPath(cwd)) + fileNames := envSpec.files + if envPresent && !filesPresent { + manager, err := metadata.Find(cwd) if err != nil { return nil, err } - libPath, envLibPath := manager.LibPaths(*env) + libPath, envLibPath := manager.LibPaths(*envSpec.env) expander.FlagJpath = append([]string{string(libPath), string(envLibPath)}, expander.FlagJpath...) fileNames, err = manager.ComponentPaths() diff --git a/cmd/show.go b/cmd/show.go index 9e4ac5c53d173ad4d0760667cfc9dec87f88262c..a01dbd14a142e2410dba40ef8176e4077d1eae61 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -16,8 +16,11 @@ package cmd import ( + "os" + "github.com/spf13/cobra" + "github.com/ksonnet/kubecfg/metadata" "github.com/ksonnet/kubecfg/pkg/kubecfg" ) @@ -45,7 +48,18 @@ var showCmd = &cobra.Command{ return err } - objs, err := expandEnvCmdObjs(cmd, args) + cwd, err := os.Getwd() + if err != nil { + return err + } + wd := metadata.AbsPath(cwd) + + envSpec, err := parseEnvCmd(cmd, args) + if err != nil { + return err + } + + objs, err := expandEnvCmdObjs(cmd, envSpec, wd) if err != nil { return err } diff --git a/cmd/update.go b/cmd/update.go index ffe4514cfa18f640bb4b41611ee6f87c509a3039..f8a8d35ae13f16e02aea1c907edd6b490cf4afe9 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -66,7 +66,13 @@ local configuration. Accepts JSON, YAML, or Jsonnet.`, return err } - c.ClientPool, c.Discovery, err = restClientPool(cmd) + cwd, err := os.Getwd() + if err != nil { + return err + } + wd := metadata.AbsPath(cwd) + + c.ClientPool, c.Discovery, err = restClientPool(cmd, nil) if err != nil { return err } @@ -76,17 +82,17 @@ local configuration. Accepts JSON, YAML, or Jsonnet.`, return err } - cwd, err := os.Getwd() + envSpec, err := parseEnvCmd(cmd, args) if err != nil { return err } - objs, err := expandEnvCmdObjs(cmd, args) + objs, err := expandEnvCmdObjs(cmd, envSpec, wd) if err != nil { return err } - return c.Run(objs, metadata.AbsPath(cwd)) + return c.Run(objs, wd) }, Long: `NOTE: Command 'update' is deprecated, use 'apply' instead. diff --git a/cmd/validate.go b/cmd/validate.go index f05f75e95ec3917bd5fbd1d4a48245f6454348ee..5e09884d9e23a869811a9d6f4ea467ce1e5f458d 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -16,8 +16,11 @@ package cmd import ( + "os" + "github.com/spf13/cobra" + "github.com/ksonnet/kubecfg/metadata" "github.com/ksonnet/kubecfg/pkg/kubecfg" ) @@ -27,19 +30,30 @@ func init() { } var validateCmd = &cobra.Command{ - Use: "validate [<env>|-f <file-or-dir>]", + Use: "validate [env-name] [-f <file-or-dir>]", Short: "Compare generated manifest against server OpenAPI spec", RunE: func(cmd *cobra.Command, args []string) error { var err error c := kubecfg.ValidateCmd{} - _, c.Discovery, err = restClientPool(cmd) + cwd, err := os.Getwd() + if err != nil { + return err + } + wd := metadata.AbsPath(cwd) + + envSpec, err := parseEnvCmd(cmd, args) if err != nil { return err } - objs, err := expandEnvCmdObjs(cmd, args) + _, c.Discovery, err = restClientPool(cmd, nil) + if err != nil { + return err + } + + objs, err := expandEnvCmdObjs(cmd, envSpec, wd) if err != nil { return err } @@ -54,11 +68,15 @@ files.`, Example: ` # Validate all resources described in a ksonnet application, expanding # ksonnet code with 'dev' environment where necessary (i.e., not YAML, JSON, # or non-ksonnet Jsonnet code). - ksonnet validate -e=dev + ksonnet validate dev # Validate resources described in a YAML file. ksonnet validate -f ./pod.yaml + # Validate resources described in the JSON file against existing resources + # in the cluster the 'dev' environment is pointing at. + ksonnet validate dev -f ./pod.yaml + # Validate resources described in a Jsonnet file. Does not expand using # environment bindings. ksonnet validate -f ./pod.jsonnet`, diff --git a/metadata/environment.go b/metadata/environment.go index 797bec47f4bef712f9d1e074fc8a5435ba080d31..8d66ea8e1069a50716c4d78d5910775659eeb103 100644 --- a/metadata/environment.go +++ b/metadata/environment.go @@ -173,8 +173,8 @@ func (m *manager) DeleteEnvironment(name string) error { return nil } -func (m *manager) GetEnvironments() ([]Environment, error) { - envs := []Environment{} +func (m *manager) GetEnvironments() ([]*Environment, error) { + envs := []*Environment{} log.Info("Retrieving all environments") err := afero.Walk(m.appFS, string(m.environmentsPath), func(path string, f os.FileInfo, err error) error { @@ -207,7 +207,7 @@ func (m *manager) GetEnvironments() ([]Environment, error) { } log.Debugf("Found environment '%s', with uri '%s", envName, envSpec.URI) - envs = append(envs, Environment{Name: envName, Path: path, URI: envSpec.URI}) + envs = append(envs, &Environment{Name: envName, Path: path, URI: envSpec.URI}) } } @@ -221,7 +221,22 @@ func (m *manager) GetEnvironments() ([]Environment, error) { return envs, nil } -func (m *manager) SetEnvironment(name string, desired Environment) error { +func (m *manager) GetEnvironment(name string) (*Environment, error) { + envs, err := m.GetEnvironments() + if err != nil { + return nil, err + } + + for _, env := range envs { + if env.Name == name { + return env, nil + } + } + + return nil, fmt.Errorf("Environment '%s' does not exist", name) +} + +func (m *manager) SetEnvironment(name string, desired *Environment) error { // Check whether this environment exists envExists, err := m.environmentExists(name) if err != nil { diff --git a/metadata/environment_test.go b/metadata/environment_test.go index 3d9c9fd3b25cb39d1e09707d895d240e4df9adfc..b94d9f16c9f1196eb050ce4b00d419034b817ac8 100644 --- a/metadata/environment_test.go +++ b/metadata/environment_test.go @@ -137,13 +137,13 @@ func TestSetEnvironment(t *testing.T) { set := Environment{Name: setName, URI: setURI} // Test updating an environment that doesn't exist - err := m.SetEnvironment("notexists", set) + err := m.SetEnvironment("notexists", &set) if err == nil { t.Fatal("Expected error when setting an environment that does not exist") } // Test updating an environment to an environment that already exists - err = m.SetEnvironment(mockEnvName, Environment{Name: mockEnvName2}) + err = m.SetEnvironment(mockEnvName, &Environment{Name: mockEnvName2}) if err == nil { t.Fatalf("Expected error when setting \"%s\" to \"%s\", because env already exists", mockEnvName, mockEnvName2) } @@ -151,7 +151,7 @@ func TestSetEnvironment(t *testing.T) { // Test changing the name and URI of a an existing environment. // Ensure new env directory is created, and old directory no longer exists. // Also ensure URI is set in spec.json - err = m.SetEnvironment(mockEnvName, set) + err = m.SetEnvironment(mockEnvName, &set) if err != nil { t.Fatalf("Could not set \"%s\", got:\n %s", mockEnvName, err) } diff --git a/metadata/interface.go b/metadata/interface.go index 2c45891ea604e1a6e35799ad5b508fc3bae0ccbb..3a1d780b784c944c81e0e1337aa0ffbe705e67c2 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -42,8 +42,9 @@ type Manager interface { LibPaths(envName string) (libPath, envLibPath AbsPath) CreateEnvironment(name, uri string, spec ClusterSpec) error DeleteEnvironment(name string) error - GetEnvironments() ([]Environment, error) - SetEnvironment(name string, desired Environment) error + GetEnvironments() ([]*Environment, error) + GetEnvironment(name string) (*Environment, error) + SetEnvironment(name string, desired *Environment) error // // TODO: Fill in methods as we need them. // diff --git a/pkg/kubecfg/env.go b/pkg/kubecfg/env.go index 7c86c2b851521befedd7d6fa13dc4f779b780b15..d78424376127760adf96786d83969e75dba5fcbf 100644 --- a/pkg/kubecfg/env.go +++ b/pkg/kubecfg/env.go @@ -30,27 +30,22 @@ type EnvAddCmd struct { name string uri string - rootPath metadata.AbsPath - spec metadata.ClusterSpec + spec metadata.ClusterSpec + manager metadata.Manager } -func NewEnvAddCmd(name, uri, specFlag string, rootPath metadata.AbsPath) (*EnvAddCmd, error) { +func NewEnvAddCmd(name, uri, specFlag string, manager metadata.Manager) (*EnvAddCmd, error) { spec, err := metadata.ParseClusterSpec(specFlag) if err != nil { return nil, err } log.Debugf("Generating ksonnetLib data with spec: %s", specFlag) - return &EnvAddCmd{name: name, uri: uri, spec: spec, rootPath: rootPath}, nil + return &EnvAddCmd{name: name, uri: uri, spec: spec, manager: manager}, nil } func (c *EnvAddCmd) Run() error { - manager, err := metadata.Find(c.rootPath) - if err != nil { - return err - } - - return manager.CreateEnvironment(c.name, c.uri, c.spec) + return c.manager.CreateEnvironment(c.name, c.uri, c.spec) } // ================================================================== @@ -58,39 +53,29 @@ func (c *EnvAddCmd) Run() error { type EnvRmCmd struct { name string - rootPath metadata.AbsPath + manager metadata.Manager } -func NewEnvRmCmd(name string, rootPath metadata.AbsPath) (*EnvRmCmd, error) { - return &EnvRmCmd{name: name, rootPath: rootPath}, nil +func NewEnvRmCmd(name string, manager metadata.Manager) (*EnvRmCmd, error) { + return &EnvRmCmd{name: name, manager: manager}, nil } func (c *EnvRmCmd) Run() error { - manager, err := metadata.Find(c.rootPath) - if err != nil { - return err - } - - return manager.DeleteEnvironment(c.name) + return c.manager.DeleteEnvironment(c.name) } // ================================================================== type EnvListCmd struct { - rootPath metadata.AbsPath + manager metadata.Manager } -func NewEnvListCmd(rootPath metadata.AbsPath) (*EnvListCmd, error) { - return &EnvListCmd{rootPath: rootPath}, nil +func NewEnvListCmd(manager metadata.Manager) (*EnvListCmd, error) { + return &EnvListCmd{manager: manager}, nil } func (c *EnvListCmd) Run(out io.Writer) error { - manager, err := metadata.Find(c.rootPath) - if err != nil { - return err - } - - envs, err := manager.GetEnvironments() + envs, err := c.manager.GetEnvironments() if err != nil { return err } @@ -133,19 +118,14 @@ type EnvSetCmd struct { desiredName string desiredURI string - rootPath metadata.AbsPath + manager metadata.Manager } -func NewEnvSetCmd(name, desiredName, desiredURI string, rootPath metadata.AbsPath) (*EnvSetCmd, error) { - return &EnvSetCmd{name: name, desiredName: desiredName, desiredURI: desiredURI, rootPath: rootPath}, nil +func NewEnvSetCmd(name, desiredName, desiredURI string, manager metadata.Manager) (*EnvSetCmd, error) { + return &EnvSetCmd{name: name, desiredName: desiredName, desiredURI: desiredURI, manager: manager}, nil } func (c *EnvSetCmd) Run() error { - manager, err := metadata.Find(c.rootPath) - if err != nil { - return err - } - desired := metadata.Environment{Name: c.desiredName, URI: c.desiredURI} - return manager.SetEnvironment(c.name, desired) + return c.manager.SetEnvironment(c.name, &desired) }