From 567773103b9cd7db316a9f36adeb4baabfa8fa6e Mon Sep 17 00:00:00 2001
From: Jessica Yuen <im.jessicayuen@gmail.com>
Date: Mon, 11 Sep 2017 10:41:51 -0700
Subject: [PATCH] Add subcommand 'env add'

'env add <env-name> <env-uri>' will create a new environment within a
ksonnet project, by generating a new directory, 'env-name', within the
'envs' directory. Each environment will contain environment-specfic
files. Notably, a new environment-specific file is 'spec.json'.
'spec.json' currently only contains the 'env-uri' of the Kubernetes
cluster located at the added environment.

Below is an example directory structure for the environment
'us-west/staging':

app-name/
  .gitignore           Default .gitignore; can customize VCS
  .ksonnet/            Metadata for ksonnet
  environments/        Env specs (defaults: dev, test, prod)
    default/           [Default generated environment.]
    us-west/           [Example of user-generated env]
      staging/
        k.libsonnet
        k8s.libsonnet
        swagger.json
        spec.json      [This will contain the uri of the environment]
  components/          Top-level Kubernetes objects defining application
  lib/                 user-written .libsonnet files
  vendor/              mixin libraries, prototypes
---
 cmd/env.go              | 110 +++++++++++++++++++++++++++++++++++++
 metadata/environment.go | 116 ++++++++++++++++++++++++++++++++++++++++
 metadata/interface.go   |   3 +-
 metadata/manager.go     |  67 ++---------------------
 pkg/kubecfg/env.go      |  51 ++++++++++++++++++
 5 files changed, 284 insertions(+), 63 deletions(-)
 create mode 100644 cmd/env.go
 create mode 100644 metadata/environment.go
 create mode 100644 pkg/kubecfg/env.go

diff --git a/cmd/env.go b/cmd/env.go
new file mode 100644
index 00000000..ddf30e06
--- /dev/null
+++ b/cmd/env.go
@@ -0,0 +1,110 @@
+// Copyright 2017 The kubecfg authors
+//
+//
+//    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 cmd
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	"github.com/ksonnet/kubecfg/metadata"
+	"github.com/ksonnet/kubecfg/pkg/kubecfg"
+)
+
+func init() {
+	RootCmd.AddCommand(envCmd)
+	envCmd.AddCommand(envAddCmd)
+	// TODO: We need to make this default to checking the `kubeconfig` file.
+	envAddCmd.PersistentFlags().String(flagAPISpec, "version:v1.7.0",
+		"Manually specify API version from OpenAPI schema, cluster, or Kubernetes version")
+}
+
+var envCmd = &cobra.Command{
+	Use:   "env",
+	Short: `Create, remove, modify, and list ksonnet environments`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return fmt.Errorf("Command 'env' requires a subcommand\n\n%s", cmd.UsageString())
+	},
+}
+
+// TODO Currently, by default, this command will overwrite the environment with
+// the same name, if it exists. May want to extend the behavior later to provide
+// a user prompt (y/n).
+var envAddCmd = &cobra.Command{
+	Use:   "add <env-name> <env-uri>",
+	Short: "Add a new environment within a ksonnet project",
+	RunE: func(cmd *cobra.Command, args []string) error {
+		flags := cmd.Flags()
+		if len(args) != 2 {
+			return fmt.Errorf("'env add' takes two arguments, the name and the uri of the environment, respectively")
+		}
+
+		envName := args[0]
+		envURI := args[1]
+
+		appDir, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		appRoot := metadata.AbsPath(appDir)
+
+		specFlag, err := flags.GetString(flagAPISpec)
+		if err != nil {
+			return err
+		}
+
+		c, err := kubecfg.NewEnvAddCmd(envName, envURI, specFlag, appRoot)
+		if err != nil {
+			return err
+		}
+
+		return c.Run()
+	},
+	Long: `Create a new environment within a ksonnet project. This will
+generate a new directory, 'env-name', within the 'environmentss' directory,
+containing the environment-specific files. 'env-uri' is the URI which the
+Kubernete's cluster is located for the added environment.
+
+Below is an example directory structure:
+
+	app-name/
+	  .gitignore             Default .gitignore; can customize VCS
+		.ksonnet/            Metadata for ksonnet
+		environments/        Env specs (defaults: dev, test, prod)
+		  default/           [Default generated environment]
+		  us-west/           [Example of user-generated env]
+			staging/
+			  k.libsonnet
+			  k8s.libsonnet
+			  swagger.json
+			  spec.json      [This will contain the uri of the environment]
+		components/          Top-level Kubernetes objects defining application
+		lib/                 user-written .libsonnet files
+		vendor/              mixin libraries, prototypes
+`,
+	Example: `  # Initialize a new staging environment at us-west. The directory
+  # structure rooted at 'us-west' in the documentation above will be generated.
+  ksonnet env add us-west/staging https://kubecfg-1.us-west.elb.amazonaws.com
+
+  # Initialize a new staging environment at us-west, using the OpenAPI specification
+  # generated in the Kubernetes v1.7.1 build to generate 'ksonnet-lib'.
+  ksonnet env add us-west/staging https://kubecfg-1.us-west.elb.amazonaws.com --api-spec=version:v1.7.1
+
+  # Initialize a new development environment locally. This will overwrite the
+  # default 'default' directory structure generated by 'ksonnet-init'.
+  ksonnet env add default localhost:8000`,
+}
diff --git a/metadata/environment.go b/metadata/environment.go
new file mode 100644
index 00000000..409e03a8
--- /dev/null
+++ b/metadata/environment.go
@@ -0,0 +1,116 @@
+// Copyright 2017 The kubecfg authors
+//
+//
+//    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 metadata
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+
+	"github.com/spf13/afero"
+
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet"
+	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
+)
+
+const (
+	defaultEnvName = "default"
+
+	schemaFilename        = "swagger.json"
+	extensionsLibFilename = "k.libsonnet"
+	k8sLibFilename        = "k8s.libsonnet"
+	specFilename          = "spec.json"
+)
+
+type Environment struct {
+	Path string
+	Name string
+	URI  string
+}
+
+type EnvironmentSpec struct {
+	URI string `json:"uri"`
+}
+
+func (m *manager) CreateEnvironment(name, uri string, spec ClusterSpec, extensionsLibData, k8sLibData []byte) error {
+	envPath := appendToAbsPath(m.environmentsDir, name)
+	err := m.appFS.MkdirAll(string(envPath), os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	// Get cluster specification data, possibly from the network.
+	specData, err := spec.data()
+	if err != nil {
+		return err
+	}
+
+	// Generate the schema file.
+	schemaPath := appendToAbsPath(envPath, schemaFilename)
+	err = afero.WriteFile(m.appFS, string(schemaPath), specData, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	k8sLibPath := appendToAbsPath(envPath, k8sLibFilename)
+	err = afero.WriteFile(m.appFS, string(k8sLibPath), k8sLibData, 0644)
+	if err != nil {
+		return err
+	}
+
+	extensionsLibPath := appendToAbsPath(envPath, extensionsLibFilename)
+	err = afero.WriteFile(m.appFS, string(extensionsLibPath), extensionsLibData, 0644)
+	if err != nil {
+		return err
+	}
+
+	// Generate the environment spec file.
+	envSpecData, err := generateSpecData(uri)
+	if err != nil {
+		return err
+	}
+
+	envSpecPath := appendToAbsPath(envPath, specFilename)
+	return afero.WriteFile(m.appFS, string(envSpecPath), envSpecData, os.ModePerm)
+}
+
+func (m *manager) GenerateKsonnetLibData(spec ClusterSpec) ([]byte, []byte, error) {
+	// Get cluster specification data, possibly from the network.
+	text, err := spec.data()
+	if err != nil {
+		return nil, nil, err
+	}
+
+	ksonnetLibDir := appendToAbsPath(m.environmentsDir, defaultEnvName)
+
+	// Deserialize the API object.
+	s := kubespec.APISpec{}
+	err = json.Unmarshal(text, &s)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	s.Text = text
+	s.FilePath = filepath.Dir(string(ksonnetLibDir))
+
+	// Emit Jsonnet code.
+	return ksonnet.Emit(&s, nil, nil)
+}
+
+func generateSpecData(uri string) ([]byte, error) {
+	// Format the spec json and return; preface keys with 2 space idents.
+	return json.MarshalIndent(EnvironmentSpec{URI: uri}, "", "  ")
+}
diff --git a/metadata/interface.go b/metadata/interface.go
index f18e1d5a..829b2ee1 100644
--- a/metadata/interface.go
+++ b/metadata/interface.go
@@ -21,13 +21,14 @@ type Manager interface {
 	Root() AbsPath
 	ComponentPaths() (AbsPaths, error)
 	LibPaths(envName string) (libPath, envLibPath AbsPath)
+	GenerateKsonnetLibData(spec ClusterSpec) ([]byte, []byte, error)
+	CreateEnvironment(name, uri string, spec ClusterSpec, extensionsLibData, k8sLibData []byte) error
 	//
 	// TODO: Fill in methods as we need them.
 	//
 	// GetPrototype(id string) Protoype
 	// SearchPrototypes(query string) []Protoype
 	// VendorLibrary(uri, version string) error
-	// CreateEnv(name string, spec *ClusterSpec) error
 	// DeleteEnv(name string) error
 	//
 }
diff --git a/metadata/manager.go b/metadata/manager.go
index a89624e9..bc8dad80 100644
--- a/metadata/manager.go
+++ b/metadata/manager.go
@@ -1,14 +1,11 @@
 package metadata
 
 import (
-	"encoding/json"
 	"fmt"
 	"os"
 	"path"
 	"path/filepath"
 
-	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet"
-	"github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec"
 	"github.com/spf13/afero"
 )
 
@@ -23,14 +20,6 @@ const (
 	componentsDir   = "components"
 	environmentsDir = "environments"
 	vendorDir       = "vendor"
-
-	defaultEnvName = "default"
-
-	// Environment-specific files
-	schemaFilename        = "swagger.json"
-	extensionsLibFilename = "k.libsonnet"
-	k8sLibFilename        = "k8s.libsonnet"
-	specFilename          = "spec.json"
 )
 
 type manager struct {
@@ -67,12 +56,6 @@ func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) {
 }
 
 func initManager(rootPath AbsPath, spec ClusterSpec, appFS afero.Fs) (*manager, error) {
-	// Get cluster specification data, possibly from the network.
-	specData, err := spec.data()
-	if err != nil {
-		return nil, err
-	}
-
 	m := newManager(rootPath, appFS)
 
 	// Generate the program text for ksonnet-lib.
@@ -82,19 +65,19 @@ func initManager(rootPath AbsPath, spec ClusterSpec, appFS afero.Fs) (*manager,
 	// either (e.g., GET'ing the spec from a live cluster returns 404) does not
 	// result in a partially-initialized directory structure.
 	//
-	ksonnetLibDir := appendToAbsPath(m.environmentsDir, defaultEnvName)
-	extensionsLibData, k8sLibData, err := generateKsonnetLibData(ksonnetLibDir, specData)
+	extensionsLibData, k8sLibData, err := m.GenerateKsonnetLibData(spec)
 	if err != nil {
 		return nil, err
 	}
 
 	// Initialize directory structure.
-	if err = m.createAppDirTree(); err != nil {
+	if err := m.createAppDirTree(); err != nil {
 		return nil, err
 	}
 
-	// Cache specification data.
-	if err = m.createEnvironment(defaultEnvName, specData, extensionsLibData, k8sLibData); err != nil {
+	// Initialize environment, and cache specification data.
+	// TODO the URI for the default environment needs to be generated from KUBECONFIG
+	if err := m.CreateEnvironment(defaultEnvName, "", spec, extensionsLibData, k8sLibData); err != nil {
 		return nil, err
 	}
 
@@ -141,31 +124,6 @@ func (m *manager) LibPaths(envName string) (libPath, envLibPath AbsPath) {
 	return m.libPath, appendToAbsPath(m.environmentsDir, envName)
 }
 
-func (m *manager) createEnvironment(name string, specData, extensionsLibData, k8sLibData []byte) error {
-	envPath := appendToAbsPath(m.environmentsDir, name)
-	err := m.appFS.MkdirAll(string(envPath), os.ModePerm)
-	if err != nil {
-		return err
-	}
-
-	// Generate the schema file.
-	schemaPath := appendToAbsPath(envPath, schemaFilename)
-	err = afero.WriteFile(m.appFS, string(schemaPath), specData, os.ModePerm)
-	if err != nil {
-		return err
-	}
-
-	k8sLibPath := appendToAbsPath(envPath, k8sLibFilename)
-	err = afero.WriteFile(m.appFS, string(k8sLibPath), k8sLibData, 0644)
-	if err != nil {
-		return err
-	}
-
-	extensionsLibPath := appendToAbsPath(envPath, extensionsLibFilename)
-	err = afero.WriteFile(m.appFS, string(extensionsLibPath), extensionsLibData, 0644)
-	return err
-}
-
 func (m *manager) createAppDirTree() error {
 	exists, err := afero.DirExists(m.appFS, string(m.rootPath))
 	if err != nil {
@@ -190,18 +148,3 @@ func (m *manager) createAppDirTree() error {
 
 	return nil
 }
-
-func generateKsonnetLibData(ksonnetLibDir AbsPath, text []byte) ([]byte, []byte, error) {
-	// Deserialize the API object.
-	s := kubespec.APISpec{}
-	err := json.Unmarshal(text, &s)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	s.Text = text
-	s.FilePath = filepath.Dir(string(ksonnetLibDir))
-
-	// Emit Jsonnet code.
-	return ksonnet.Emit(&s, nil, nil)
-}
diff --git a/pkg/kubecfg/env.go b/pkg/kubecfg/env.go
new file mode 100644
index 00000000..795f7aa0
--- /dev/null
+++ b/pkg/kubecfg/env.go
@@ -0,0 +1,51 @@
+// Copyright 2017 The kubecfg authors
+//
+//
+//    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 kubecfg
+
+import (
+	"github.com/ksonnet/kubecfg/metadata"
+)
+
+type EnvAddCmd struct {
+	name string
+	uri  string
+
+	rootPath metadata.AbsPath
+	spec     metadata.ClusterSpec
+}
+
+func NewEnvAddCmd(name, uri, specFlag string, rootPath metadata.AbsPath) (*EnvAddCmd, error) {
+	spec, err := metadata.ParseClusterSpec(specFlag)
+	if err != nil {
+		return nil, err
+	}
+
+	return &EnvAddCmd{name: name, uri: uri, spec: spec, rootPath: rootPath}, nil
+}
+
+func (c *EnvAddCmd) Run() error {
+	manager, err := metadata.Find(c.rootPath)
+	if err != nil {
+		return err
+	}
+
+	extensionsLibData, k8sLibData, err := manager.GenerateKsonnetLibData(c.spec)
+	if err != nil {
+		return err
+	}
+
+	return manager.CreateEnvironment(c.name, c.uri, c.spec, extensionsLibData, k8sLibData)
+}
-- 
GitLab