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