diff --git a/cmd/diff.go b/cmd/diff.go index 4d758544f0124ac8559c6d181fbf440f2e09d147..13bf1b8128c4bd27faac1c9f44f953eb71bb8e7b 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -321,8 +321,11 @@ func expandEnvObjs(fs afero.Fs, cmd *cobra.Command, env string, manager metadata return nil, err } - libPath, vendorPath := manager.LibPaths() - metadataPath, mainPath, paramsPath := manager.EnvPaths(env) + _, vendorPath := manager.LibPaths() + libPath, mainPath, paramsPath, err := manager.EnvPaths(env) + if err != nil { + return nil, err + } componentPaths, err := manager.ComponentPaths() if err != nil { return nil, err @@ -338,7 +341,7 @@ func expandEnvObjs(fs afero.Fs, cmd *cobra.Command, env string, manager metadata return nil, err } - expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(metadataPath)}, expander.FlagJpath...) + expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...) expander.ExtCodes = append([]string{baseObj, params, envSpec}, expander.ExtCodes...) envFiles := []string{string(mainPath)} diff --git a/cmd/init.go b/cmd/init.go index 778f1f236511ceac72424c3f20cff3873ed1406d..b8361112e8ae4f5df52b37754b01b78e34a444ba 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -77,7 +77,7 @@ var initCmd = &cobra.Command{ } log.Infof("Creating a new app '%s' at path '%s'", appName, appRoot) - c, err := kubecfg.NewInitCmd(appName, appRoot, specFlag, &server, &namespace) + c, err := kubecfg.NewInitCmd(appName, appRoot, &specFlag, &server, &namespace) if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index d5525c302fa61854acdf3a91c954fdd88e04f746..9dd9f72245b08a3f736b2ba90bcdf20dcb6b0a60 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -432,10 +432,13 @@ func (te *cmdObjExpander) Expand() ([]*unstructured.Unstructured, error) { return nil, errors.Wrap(err, "find metadata") } - libPath, vendorPath := manager.LibPaths() - metadataPath, mainPath, paramsPath := manager.EnvPaths(te.config.env) + _, vendorPath := manager.LibPaths() + libPath, mainPath, paramsPath, err := manager.EnvPaths(te.config.env) + if err != nil { + return nil, err + } - expander.FlagJpath = append([]string{string(libPath), string(vendorPath), string(metadataPath)}, expander.FlagJpath...) + expander.FlagJpath = append([]string{string(vendorPath), string(libPath)}, expander.FlagJpath...) componentPaths, err := manager.ComponentPaths() if err != nil { diff --git a/integration/fixtures/sampleapp/environments/default/.metadata/k.libsonnet b/integration/fixtures/sampleapp/lib/v1.7.0/k.libsonnet similarity index 100% rename from integration/fixtures/sampleapp/environments/default/.metadata/k.libsonnet rename to integration/fixtures/sampleapp/lib/v1.7.0/k.libsonnet diff --git a/integration/fixtures/sampleapp/environments/default/.metadata/k8s.libsonnet b/integration/fixtures/sampleapp/lib/v1.7.0/k8s.libsonnet similarity index 100% rename from integration/fixtures/sampleapp/environments/default/.metadata/k8s.libsonnet rename to integration/fixtures/sampleapp/lib/v1.7.0/k8s.libsonnet diff --git a/integration/fixtures/sampleapp/environments/default/.metadata/swagger.json b/integration/fixtures/sampleapp/lib/v1.7.0/swagger.json similarity index 100% rename from integration/fixtures/sampleapp/environments/default/.metadata/swagger.json rename to integration/fixtures/sampleapp/lib/v1.7.0/swagger.json diff --git a/integration/integration_suite_test.go b/integration/integration_suite_test.go index 76998eadb0f6c94f39cf4bb6a4fea948d5e39d42..d848136eb1d326a90cd5245f8f7785c65033056b 100644 --- a/integration/integration_suite_test.go +++ b/integration/integration_suite_test.go @@ -100,6 +100,7 @@ func runKsonnetWith(flags []string, host, ns string) error { Server: host, }, }, + KubernetesVersion: "v1.7.0", }, }, } diff --git a/metadata/clusterspec.go b/metadata/clusterspec.go deleted file mode 100644 index 212731a43a7495df3c0bba478ceb93e16326e91b..0000000000000000000000000000000000000000 --- a/metadata/clusterspec.go +++ /dev/null @@ -1,87 +0,0 @@ -package metadata - -import ( - "fmt" - "io/ioutil" - "net/http" - "path/filepath" - "strings" - - "github.com/spf13/afero" -) - -const ( - k8sVersionURLTemplate = "https://raw.githubusercontent.com/kubernetes/kubernetes/%s/api/openapi-spec/swagger.json" -) - -func parseClusterSpec(specFlag string, fs afero.Fs) (ClusterSpec, error) { - split := strings.SplitN(specFlag, ":", 2) - if len(split) <= 1 || split[1] == "" { - return nil, fmt.Errorf("Invalid API specification '%s'", specFlag) - } - - switch split[0] { - case "version": - return &clusterSpecVersion{k8sVersion: split[1]}, nil - case "file": - p, err := filepath.Abs(split[1]) - if err != nil { - return nil, err - } - return &clusterSpecFile{specPath: p, fs: fs}, nil - case "url": - return &clusterSpecLive{apiServerURL: split[1]}, nil - default: - return nil, fmt.Errorf("Could not parse cluster spec '%s'", specFlag) - } -} - -type clusterSpecFile struct { - specPath string - fs afero.Fs -} - -func (cs *clusterSpecFile) OpenAPI() ([]byte, error) { - return afero.ReadFile(cs.fs, string(cs.specPath)) -} - -func (cs *clusterSpecFile) Resource() string { - return string(cs.specPath) -} - -type clusterSpecLive struct { - apiServerURL string -} - -func (cs *clusterSpecLive) OpenAPI() ([]byte, error) { - return nil, fmt.Errorf("Initializing from OpenAPI spec in live cluster is not implemented") -} - -func (cs *clusterSpecLive) Resource() string { - return string(cs.apiServerURL) -} - -type clusterSpecVersion struct { - k8sVersion string -} - -func (cs *clusterSpecVersion) OpenAPI() ([]byte, error) { - versionURL := fmt.Sprintf(k8sVersionURLTemplate, cs.k8sVersion) - resp, err := http.Get(versionURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf( - "Recieved status code '%d' when trying to retrieve OpenAPI schema for cluster version '%s' from URL '%s'", - resp.StatusCode, cs.k8sVersion, versionURL) - } - - return ioutil.ReadAll(resp.Body) -} - -func (cs *clusterSpecVersion) Resource() string { - return string(cs.k8sVersion) -} diff --git a/metadata/component_test.go b/metadata/component_test.go index 1d0b27198a8bf4d58f0b53f0e8c668fea9981766..4027284e6c61e037b0975b74c772f2fb01b1a1d9 100644 --- a/metadata/component_test.go +++ b/metadata/component_test.go @@ -33,14 +33,11 @@ const ( ) func populateComponentPaths(t *testing.T) *manager { - spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) - if err != nil { - t.Fatalf("Failed to parse cluster spec: %v", err) - } + specFlag := fmt.Sprintf("file:%s", blankSwagger) appPath := componentsPath reg := newMockRegistryManager("incubator") - m, err := initManager("componentPaths", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS) + m, err := initManager("componentPaths", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS) if err != nil { t.Fatalf("Failed to init cluster spec: %v", err) } diff --git a/metadata/environment.go b/metadata/environment.go index 9b5a565d062f57d7272e1565f168c704d0ac8ab4..73900d805600d38b77aa14bf9732030f0ec41b4c 100644 --- a/metadata/environment.go +++ b/metadata/environment.go @@ -22,57 +22,42 @@ import ( "path/filepath" "github.com/ksonnet/ksonnet/metadata/app" + "github.com/ksonnet/ksonnet/metadata/lib" str "github.com/ksonnet/ksonnet/strings" log "github.com/sirupsen/logrus" "github.com/spf13/afero" - "github.com/ksonnet/ksonnet/generator" param "github.com/ksonnet/ksonnet/metadata/params" ) const ( - defaultEnvName = "default" - metadataDirName = ".metadata" - - // hidden metadata files - schemaFilename = "swagger.json" - extensionsLibFilename = "k.libsonnet" - k8sLibFilename = "k8s.libsonnet" + defaultEnvName = "default" // primary environment files envFileName = "main.jsonnet" paramsFileName = "params.libsonnet" - specFilename = "spec.json" ) var envPaths = []string{ - // metadata Dir.wh - metadataDirName, // environment base override file envFileName, // params file paramsFileName, - // spec file - specFilename, } -func (m *manager) CreateEnvironment(name, server, namespace string, spec ClusterSpec) error { - b, err := spec.OpenAPI() +func (m *manager) CreateEnvironment(name, server, namespace, k8sSpecFlag string) error { + // generate the lib data for this kubernetes version + libManager, err := lib.NewManagerWithSpec(k8sSpecFlag, m.appFS, m.libPath) if err != nil { return err } - kl, err := generator.Ksonnet(b) - if err != nil { - log.Debugf("Failed to write '%s'", specFilename) + if err := libManager.GenerateLibData(); err != nil { return err } - return m.createEnvironment(name, server, namespace, kl.K, kl.K8s, kl.Swagger) -} - -func (m *manager) createEnvironment(name, server, namespace string, extensionsLibData, k8sLibData, specData []byte) error { + // add the environment to the app spec appSpec, err := m.AppSpec() if err != nil { return err @@ -99,33 +84,10 @@ func (m *manager) createEnvironment(name, server, namespace string, extensionsLi return err } - metadataPath := str.AppendToPath(envPath, metadataDirName) - err = m.appFS.MkdirAll(metadataPath, defaultFolderPermissions) - if err != nil { - return err - } - - log.Infof("Generating environment metadata at path '%s'", envPath) - metadata := []struct { path string data []byte }{ - { - // schema file - str.AppendToPath(metadataPath, schemaFilename), - specData, - }, - { - // k8s file - str.AppendToPath(metadataPath, k8sLibFilename), - k8sLibData, - }, - { - // extensions file - str.AppendToPath(metadataPath, extensionsLibFilename), - extensionsLibData, - }, { // environment base override file str.AppendToPath(envPath, envFileName), @@ -157,7 +119,7 @@ func (m *manager) createEnvironment(name, server, namespace string, extensionsLi Namespace: namespace, }, }, - // TODO specify k8s version once metadata is moved. + KubernetesVersion: libManager.K8sVersion, }) if err != nil { @@ -391,6 +353,31 @@ func (m *manager) SetEnvironmentParams(env, component string, params param.Param return nil } +func (m *manager) EnvPaths(env string) (libPath, mainPath, paramsPath string, err error) { + app, err := m.AppSpec() + if err != nil { + return + } + + envSpec, ok := app.GetEnvironmentSpec(env) + if !ok { + err = fmt.Errorf("Environment '%s' does not exist", env) + return + } + + libManager := lib.NewManager(envSpec.KubernetesVersion, m.appFS, m.libPath) + + envPath := str.AppendToPath(m.environmentsPath, env) + + // main.jsonnet file + mainPath = str.AppendToPath(envPath, envFileName) + // params.libsonnet file + paramsPath = str.AppendToPath(envPath, componentParamsFile) + // ksonnet-lib file directory + libPath, err = libManager.GetLibPath() + return +} + func (m *manager) tryMvEnvDir(dirPathOld, dirPathNew string) error { // first ensure none of these paths exists in the new directory for _, p := range envPaths { @@ -450,7 +437,7 @@ func (m *manager) generateOverrideData() []byte { var buf bytes.Buffer buf.WriteString(fmt.Sprintf("local base = import \"%s\";\n", relBaseLibsonnetPath)) - buf.WriteString(fmt.Sprintf("local k = import \"%s\";\n\n", extensionsLibFilename)) + buf.WriteString(fmt.Sprintf("local k = import \"%s\";\n\n", lib.ExtensionsLibFilename)) buf.WriteString("base + {\n") buf.WriteString(" // Insert user-specified overrides here. For example if a component is named \"nginx-deployment\", you might have something like:\n") buf.WriteString(" // \"nginx-deployment\"+: k.deployment.mixin.metadata.labels({foo: \"bar\"})\n") diff --git a/metadata/environment_test.go b/metadata/environment_test.go index 52a0e4e0a383d2a3e7124a4d51ecc739b45a2aa4..017ef0ef3457b0fde8e80e5e087dad12d6a89947 100644 --- a/metadata/environment_test.go +++ b/metadata/environment_test.go @@ -45,13 +45,10 @@ func mockEnvironments(t *testing.T, appName string) *manager { } func mockEnvironmentsWith(t *testing.T, appName string, envNames []string) *manager { - spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) - if err != nil { - t.Fatalf("Failed to parse cluster spec: %v", err) - } + specFlag := fmt.Sprintf("file:%s", blankSwagger) reg := newMockRegistryManager("incubator") - m, err := initManager(appName, appName, spec, &mockAPIServer, &mockNamespace, reg, testFS) + m, err := initManager(appName, appName, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS) if err != nil { t.Fatalf("Failed to init cluster spec: %v", err) } diff --git a/metadata/interface.go b/metadata/interface.go index b9246357feedec2b3d489a5021e420572e02c3ac..9fe0996a4d33ce15ffcce6bddde62db971aea3ee 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -38,7 +38,7 @@ var defaultFilePermissions = os.FileMode(0644) type Manager interface { Root() string LibPaths() (libPath, vendorPath string) - EnvPaths(env string) (metadataPath, mainPath, paramsPath string) + EnvPaths(env string) (libPath, mainPath, paramsPath string, err error) // Components API. ComponentPaths() ([]string, error) @@ -58,7 +58,7 @@ type Manager interface { SetEnvironmentParams(env, component string, params param.Params) error // Environment API. - CreateEnvironment(name, uri, namespace string, spec ClusterSpec) error + CreateEnvironment(name, uri, namespace, spec string) error DeleteEnvironment(name string) error GetEnvironments() (app.EnvironmentSpecs, error) GetEnvironment(name string) (*app.EnvironmentSpec, error) @@ -84,10 +84,8 @@ func Find(path string) (Manager, error) { return findManager(path, afero.NewOsFs()) } -// Init will retrieve a cluster API specification, generate a -// capabilities-compliant version of ksonnet-lib, and then generate the -// directory tree for an application. -func Init(name, rootPath string, spec ClusterSpec, serverURI, namespace *string) (Manager, error) { +// Init will generate the directory tree for a ksonnet project. +func Init(name, rootPath string, k8sSpecFlag, serverURI, namespace *string) (Manager, error) { // Generate `incubator` registry. We do this before before creating // directory tree, in case the network call fails. const ( @@ -104,24 +102,7 @@ func Init(name, rootPath string, spec ClusterSpec, serverURI, namespace *string) return nil, err } - return initManager(name, rootPath, spec, serverURI, namespace, gh, appFS) -} - -// ClusterSpec represents the API supported by some cluster. There are several -// ways to specify a cluster, including: querying the API server, reading an -// OpenAPI spec in some file, or consulting the OpenAPI spec released in a -// specific version of Kubernetes. -type ClusterSpec interface { - OpenAPI() ([]byte, error) - Resource() string // For testing parsing logic. -} - -// ParseClusterSpec will parse a cluster spec flag and output a well-formed -// ClusterSpec object. For example, if the flag is `--version:v1.7.1`, then we -// will output a ClusterSpec representing the cluster specification associated -// with the `v1.7.1` build of Kubernetes. -func ParseClusterSpec(specFlag string) (ClusterSpec, error) { - return parseClusterSpec(specFlag, appFS) + return initManager(name, rootPath, k8sSpecFlag, serverURI, namespace, gh, appFS) } // isValidName returns true if a name (e.g., for an environment) is valid. diff --git a/metadata/lib/clusterspec.go b/metadata/lib/clusterspec.go new file mode 100644 index 0000000000000000000000000000000000000000..6a0e45d0c6d429ff3c5910b0a8ec56a1cd873eae --- /dev/null +++ b/metadata/lib/clusterspec.go @@ -0,0 +1,151 @@ +// Copyright 2017 The ksonnet 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 lib + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "strings" + + "github.com/spf13/afero" +) + +const ( + k8sVersionURLTemplate = "https://raw.githubusercontent.com/kubernetes/kubernetes/%s/api/openapi-spec/swagger.json" +) + +// ClusterSpec represents the API supported by some cluster. There are several +// ways to specify a cluster, including: querying the API server, reading an +// OpenAPI spec in some file, or consulting the OpenAPI spec released in a +// specific version of Kubernetes. +type ClusterSpec interface { + OpenAPI() ([]byte, error) + Resource() string // For testing parsing logic. + Version() (string, error) +} + +// ParseClusterSpec will parse a cluster spec flag and output a well-formed +// ClusterSpec object. For example, if the flag is `--version:v1.7.1`, then we +// will output a ClusterSpec representing the cluster specification associated +// with the `v1.7.1` build of Kubernetes. +func ParseClusterSpec(specFlag string, fs afero.Fs) (ClusterSpec, error) { + split := strings.SplitN(specFlag, ":", 2) + if len(split) <= 1 || split[1] == "" { + return nil, fmt.Errorf("Invalid API specification '%s'", specFlag) + } + + switch split[0] { + case "version": + return &clusterSpecVersion{k8sVersion: split[1]}, nil + case "file": + p, err := filepath.Abs(split[1]) + if err != nil { + return nil, err + } + return &clusterSpecFile{specPath: p, fs: fs}, nil + case "url": + return &clusterSpecLive{apiServerURL: split[1]}, nil + default: + return nil, fmt.Errorf("Could not parse cluster spec '%s'", specFlag) + } +} + +type clusterSpecFile struct { + specPath string + fs afero.Fs +} + +func (cs *clusterSpecFile) OpenAPI() ([]byte, error) { + return afero.ReadFile(cs.fs, string(cs.specPath)) +} + +func (cs *clusterSpecFile) Resource() string { + return string(cs.specPath) +} + +func (cs *clusterSpecFile) Version() (string, error) { + // + // Condensed representation of the spec file, containing the minimal + // information necessary to retrieve the spec version. + // + type Info struct { + Version string `json:"version"` + } + + type Spec struct { + Info Info `json:"info"` + } + + bytes, err := cs.OpenAPI() + if err != nil { + return "", err + } + + var spec *Spec + if err := json.Unmarshal(bytes, &spec); err != nil { + return "", err + } + + return spec.Info.Version, nil +} + +type clusterSpecLive struct { + apiServerURL string +} + +func (cs *clusterSpecLive) OpenAPI() ([]byte, error) { + return nil, fmt.Errorf("Initializing from OpenAPI spec in live cluster is not implemented") +} + +func (cs *clusterSpecLive) Resource() string { + return string(cs.apiServerURL) +} + +func (cs *clusterSpecLive) Version() (string, error) { + return "", fmt.Errorf("Retrieving version spec in live cluster is not implemented") +} + +type clusterSpecVersion struct { + k8sVersion string +} + +func (cs *clusterSpecVersion) OpenAPI() ([]byte, error) { + versionURL := fmt.Sprintf(k8sVersionURLTemplate, cs.k8sVersion) + resp, err := http.Get(versionURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf( + "Recieved status code '%d' when trying to retrieve OpenAPI schema for cluster version '%s' from URL '%s'", + resp.StatusCode, cs.k8sVersion, versionURL) + } + + return ioutil.ReadAll(resp.Body) +} + +func (cs *clusterSpecVersion) Resource() string { + return string(cs.k8sVersion) +} + +func (cs *clusterSpecVersion) Version() (string, error) { + return string(cs.k8sVersion), nil +} diff --git a/metadata/clusterspec_test.go b/metadata/lib/clusterspec_test.go similarity index 73% rename from metadata/clusterspec_test.go rename to metadata/lib/clusterspec_test.go index f300b128cefdc32dd3362a17361c5894bab4a8fc..5c095cbf0c6aa33cfd41f330a381290ca630abc3 100644 --- a/metadata/clusterspec_test.go +++ b/metadata/lib/clusterspec_test.go @@ -1,4 +1,19 @@ -package metadata +// Copyright 2017 The ksonnet 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 lib import ( "path/filepath" @@ -18,7 +33,7 @@ var successTests = []parseSuccess{ func TestClusterSpecParsingSuccess(t *testing.T) { for _, test := range successTests { - parsed, err := parseClusterSpec(test.input, testFS) + parsed, err := ParseClusterSpec(test.input, testFS) if err != nil { t.Errorf("Failed to parse spec: %v", err) } @@ -66,7 +81,7 @@ var failureTests = []parseFailure{ func TestClusterSpecParsingFailure(t *testing.T) { for _, test := range failureTests { - _, err := parseClusterSpec(test.input, testFS) + _, err := ParseClusterSpec(test.input, testFS) if err == nil { t.Errorf("Cluster spec parse for '%s' should have failed, but succeeded", test.input) } else if msg := err.Error(); msg != test.errorMsg { diff --git a/metadata/lib/lib.go b/metadata/lib/lib.go new file mode 100644 index 0000000000000000000000000000000000000000..df5fd18600e4a236c582b18060a04475566e18f1 --- /dev/null +++ b/metadata/lib/lib.go @@ -0,0 +1,154 @@ +// Copyright 2017 The ksonnet 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 lib + +import ( + "fmt" + "os" + "path" + + str "github.com/ksonnet/ksonnet/strings" + log "github.com/sirupsen/logrus" + "github.com/spf13/afero" + + "github.com/ksonnet/ksonnet/generator" +) + +const ( + schemaFilename = "swagger.json" + k8sLibFilename = "k8s.libsonnet" + + // ExtensionsLibFilename is the file name with the contents of the + // generated ksonnet-lib + ExtensionsLibFilename = "k.libsonnet" +) + +// Manager operates on the files in the lib directory of a ksonnet project. +// This included generating the ksonnet-lib files needed to compile the +// ksonnet (jsonnet) code. +type Manager struct { + // K8sVersion is the Kubernetes version of the Open API spec. + K8sVersion string + + spec ClusterSpec + libPath string + fs afero.Fs +} + +// NewManager creates a crew instance of lib.Manager +func NewManager(k8sVersion string, fs afero.Fs, libPath string) *Manager { + return &Manager{K8sVersion: k8sVersion, fs: fs, libPath: libPath} +} + +// NewManagerWithSpec creates a new instance of lib.Manager with the cluster spec initialized. +func NewManagerWithSpec(k8sSpecFlag string, fs afero.Fs, libPath string) (*Manager, error) { + // + // Generate the program text for ksonnet-lib. + // + spec, err := ParseClusterSpec(k8sSpecFlag, fs) + if err != nil { + return nil, err + } + + version, err := spec.Version() + if err != nil { + return nil, err + } + + return &Manager{K8sVersion: version, fs: fs, libPath: libPath, spec: spec}, nil +} + +// GenerateLibData will generate the swagger and ksonnet-lib files in the lib +// directory of a ksonnet project. The swagger and ksonnet-lib files are +// unique to each Kubernetes API version. If the files already exist for a +// specific Kubernetes API version, they won't be re-generated here. +func (m *Manager) GenerateLibData() error { + if m.spec == nil { + return fmt.Errorf("Uninitialized ClusterSpec") + } + + b, err := m.spec.OpenAPI() + if err != nil { + return err + } + + kl, err := generator.Ksonnet(b) + if err != nil { + return err + } + + versionPath := str.AppendToPath(m.libPath, m.K8sVersion) + ok, err := afero.DirExists(m.fs, string(versionPath)) + if err != nil { + return err + } + if ok { + // Already have lib data for this k8s api version + return nil + } + + err = m.fs.MkdirAll(string(versionPath), os.FileMode(0755)) + if err != nil { + return err + } + + files := []struct { + path string + data []byte + }{ + { + // schema file + str.AppendToPath(versionPath, schemaFilename), + kl.Swagger, + }, + { + // k8s file + str.AppendToPath(versionPath, k8sLibFilename), + kl.K8s, + }, + { + // extensions file + str.AppendToPath(versionPath, ExtensionsLibFilename), + kl.K, + }, + } + + log.Infof("Generating ksonnet-lib data at path '%s'", versionPath) + + for _, a := range files { + fileName := path.Base(string(a.path)) + if err = afero.WriteFile(m.fs, string(a.path), a.data, os.FileMode(0644)); err != nil { + log.Debugf("Failed to write '%s'", fileName) + return err + } + } + + return nil +} + +// GetLibPath returns the absolute path pointing to the directory with the +// metadata files for the provided k8sVersion. +func (m *Manager) GetLibPath() (string, error) { + path := str.AppendToPath(m.libPath, m.K8sVersion) + ok, err := afero.DirExists(m.fs, string(path)) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("Expected lib directory '%s' but was not found", m.K8sVersion) + } + return path, err +} diff --git a/metadata/lib/lib_test.go b/metadata/lib/lib_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b0792e9de22c7da5b65875164f3f9fb6b040202a --- /dev/null +++ b/metadata/lib/lib_test.go @@ -0,0 +1,88 @@ +// Copyright 2017 The ksonnet 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 lib + +import ( + "fmt" + "os" + "testing" + + "github.com/spf13/afero" + + str "github.com/ksonnet/ksonnet/strings" +) + +const ( + blankSwagger = "/blankSwagger.json" + blankSwaggerData = `{ + "swagger": "2.0", + "info": { + "title": "Kubernetes", + "version": "v1.7.0" + }, + "paths": { + }, + "definitions": { + } +}` + blankK8sLib = `// AUTOGENERATED from the Kubernetes OpenAPI specification. DO NOT MODIFY. +// Kubernetes version: v1.7.0 + +{ + local hidden = { + }, +} +` +) + +var testFS = afero.NewMemMapFs() + +func init() { + afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm) +} + +func TestGenerateLibData(t *testing.T) { + specFlag := fmt.Sprintf("file:%s", blankSwagger) + libPath := "lib" + + libManager, err := NewManagerWithSpec(specFlag, testFS, libPath) + if err != nil { + t.Fatal("Failed to initialize lib.Manager") + } + + err = libManager.GenerateLibData() + if err != nil { + t.Fatal("Failed to generate lib data") + } + + // Verify contents of lib. + versionPath := str.AppendToPath(libPath, "v1.7.0") + + schemaPath := str.AppendToPath(versionPath, schemaFilename) + bytes, err := afero.ReadFile(testFS, string(schemaPath)) + if err != nil { + t.Fatalf("Failed to read swagger file at '%s':\n%v", schemaPath, err) + } else if actualSwagger := string(bytes); actualSwagger != blankSwaggerData { + t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger) + } + + k8sLibPath := str.AppendToPath(versionPath, k8sLibFilename) + k8sLibBytes, err := afero.ReadFile(testFS, string(k8sLibPath)) + if err != nil { + t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", k8sLibPath, err) + } else if actualK8sLib := string(k8sLibBytes); actualK8sLib != blankK8sLib { + t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", k8sLibPath, blankK8sLib, actualK8sLib) + } +} diff --git a/metadata/manager.go b/metadata/manager.go index f4c2420adf9001c921fe620507c2159a90f61649..6c751d4545ce17ed9effe5b06b74c2002f3313fc 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -21,7 +21,6 @@ import ( "path" "path/filepath" - "github.com/ksonnet/ksonnet/generator" "github.com/ksonnet/ksonnet/metadata/app" "github.com/ksonnet/ksonnet/metadata/registry" str "github.com/ksonnet/ksonnet/strings" @@ -99,30 +98,12 @@ func findManager(p string, appFS afero.Fs) (*manager, error) { } } -func initManager(name, rootPath string, spec ClusterSpec, serverURI, namespace *string, incubatorReg registry.Manager, appFS afero.Fs) (*manager, error) { +func initManager(name, rootPath string, k8sSpecFlag, serverURI, namespace *string, incubatorReg registry.Manager, appFS afero.Fs) (*manager, error) { m, err := newManager(rootPath, appFS) if err != nil { return nil, err } - // - // Generate the program text for ksonnet-lib. - // - // IMPLEMENTATION NOTE: We get the cluster specification and generate - // ksonnet-lib before initializing the directory structure so that failure of - // either (e.g., GET'ing the spec from a live cluster returns 404) does not - // result in a partially-initialized directory structure. - // - b, err := spec.OpenAPI() - if err != nil { - return nil, err - } - - kl, err := generator.Ksonnet(b) - if err != nil { - return nil, err - } - // Retrieve `registry.yaml`. registryYAMLData, err := generateRegistryYAMLData(incubatorReg) if err != nil { @@ -150,7 +131,7 @@ func initManager(name, rootPath string, spec ClusterSpec, serverURI, namespace * // Initialize environment, and cache specification data. if serverURI != nil { - err := m.createEnvironment(defaultEnvName, *serverURI, *namespace, kl.K, kl.K8s, kl.Swagger) + err := m.CreateEnvironment(defaultEnvName, *serverURI, *namespace, *k8sSpecFlag) if err != nil { return nil, errorOnCreateFailure(name, err) } @@ -203,19 +184,6 @@ func (m *manager) LibPaths() (libPath, vendorPath string) { return m.libPath, m.vendorPath } -func (m *manager) EnvPaths(env string) (metadataPath, mainPath, paramsPath string) { - envPath := str.AppendToPath(m.environmentsPath, env) - - // .metadata directory - metadataPath = str.AppendToPath(envPath, metadataDirName) - // main.jsonnet file - mainPath = str.AppendToPath(envPath, envFileName) - // params.libsonnet file - paramsPath = str.AppendToPath(envPath, componentParamsFile) - - return -} - func (m *manager) createUserDirTree() error { dirPaths := []string{ m.userKsonnetRootPath, diff --git a/metadata/manager_test.go b/metadata/manager_test.go index 0a8728f6a2585e38a283c6d5491850b52ddfbb28..a8f02018acbd50f6fee94139c6fa520eaaa3de12 100644 --- a/metadata/manager_test.go +++ b/metadata/manager_test.go @@ -55,14 +55,11 @@ func init() { } func TestInitSuccess(t *testing.T) { - spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) - if err != nil { - t.Fatalf("Failed to parse cluster spec: %v", err) - } + specFlag := fmt.Sprintf("file:%s", blankSwagger) appPath := "/fromEmptySwagger" reg := newMockRegistryManager("incubator") - _, err = initManager("fromEmptySwagger", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS) + _, err := initManager("fromEmptySwagger", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS) if err != nil { t.Fatalf("Failed to init cluster spec: %v", err) } @@ -110,31 +107,6 @@ func TestInitSuccess(t *testing.T) { // Verify contents of metadata. envPath := str.AppendToPath(appPath, environmentsDir) - metadataPath := str.AppendToPath(appPath, defaultEnvDir, metadataDirName) - - schemaPath := str.AppendToPath(metadataPath, schemaFilename) - bytes, err := afero.ReadFile(testFS, schemaPath) - if err != nil { - t.Fatalf("Failed to read swagger file at '%s':\n%v", schemaPath, err) - } else if actualSwagger := string(bytes); actualSwagger != blankSwaggerData { - t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger) - } - - k8sLibPath := str.AppendToPath(metadataPath, k8sLibFilename) - k8sLibBytes, err := afero.ReadFile(testFS, k8sLibPath) - if err != nil { - t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", k8sLibPath, err) - } else if actualK8sLib := string(k8sLibBytes); actualK8sLib != blankK8sLib { - t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", k8sLibPath, blankK8sLib, actualK8sLib) - } - - extensionsLibPath := str.AppendToPath(metadataPath, extensionsLibFilename) - extensionsLibBytes, err := afero.ReadFile(testFS, extensionsLibPath) - if err != nil { - t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", extensionsLibPath, err) - } else if string(extensionsLibBytes) == "" { - t.Fatalf("Expected extension library file at '%s' to be non-empty", extensionsLibPath) - } componentParamsPath := str.AppendToPath(appPath, componentsDir, componentParamsFile) componentParamsBytes, err := afero.ReadFile(testFS, componentParamsPath) @@ -179,14 +151,11 @@ func TestFindSuccess(t *testing.T) { } } - spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) - if err != nil { - t.Fatalf("Failed to parse cluster spec: %v", err) - } + specFlag := fmt.Sprintf("file:%s", blankSwagger) appPath := "/findSuccess" reg := newMockRegistryManager("incubator") - _, err = initManager("findSuccess", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS) + _, err := initManager("findSuccess", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS) if err != nil { t.Fatalf("Failed to init cluster spec: %v", err) } @@ -224,16 +193,15 @@ func TestLibPaths(t *testing.T) { func TestEnvPaths(t *testing.T) { appName := "test-env-paths" - expectedMetadataPath := path.Join(appName, environmentsDir, mockEnvName, metadataDirName) expectedMainPath := path.Join(appName, environmentsDir, mockEnvName, envFileName) expectedParamsPath := path.Join(appName, environmentsDir, mockEnvName, paramsFileName) m := mockEnvironments(t, appName) - metadataPath, mainPath, paramsPath := m.EnvPaths(mockEnvName) - - if metadataPath != expectedMetadataPath { - t.Fatalf("Expected environment metadata dir path to be:\n '%s'\n, got:\n '%s'", expectedMetadataPath, metadataPath) + _, mainPath, paramsPath, err := m.EnvPaths(mockEnvName) + if err != nil { + t.Fatalf("Failure retrieving EnvPaths") } + if mainPath != expectedMainPath { t.Fatalf("Expected environment main path to be:\n '%s'\n, got:\n '%s'", expectedMainPath, mainPath) } @@ -256,20 +224,17 @@ func TestFindFailure(t *testing.T) { } func TestDoubleNewFailure(t *testing.T) { - spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) - if err != nil { - t.Fatalf("Failed to parse cluster spec: %v", err) - } + specFlag := fmt.Sprintf("file:%s", blankSwagger) appPath := "/doubleNew" reg := newMockRegistryManager("incubator") - _, err = initManager("doubleNew", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS) + _, err := initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS) if err != nil { t.Fatalf("Failed to init cluster spec: %v", err) } targetErr := fmt.Sprintf("Could not create app; directory '%s' already exists", appPath) - _, err = initManager("doubleNew", appPath, spec, &mockAPIServer, &mockNamespace, reg, testFS) + _, err = initManager("doubleNew", appPath, &specFlag, &mockAPIServer, &mockNamespace, reg, testFS) if err == nil || err.Error() != targetErr { t.Fatalf("Expected to fail to create app with message '%s', got '%s'", targetErr, err.Error()) } diff --git a/pkg/kubecfg/env.go b/pkg/kubecfg/env.go index a68a8f161766a348eb1a02de96dd0cfeb51c1505..c990fda88d3fda2e6a0d4414eebe0ced8abd60bb 100644 --- a/pkg/kubecfg/env.go +++ b/pkg/kubecfg/env.go @@ -23,8 +23,6 @@ import ( "github.com/ksonnet/ksonnet/metadata/app" - log "github.com/sirupsen/logrus" - "github.com/ksonnet/ksonnet/metadata" str "github.com/ksonnet/ksonnet/strings" ) @@ -33,19 +31,13 @@ type EnvAddCmd struct { name string server string namespace string + spec string - spec metadata.ClusterSpec manager metadata.Manager } func NewEnvAddCmd(name, server, namespace, 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, server: server, namespace: namespace, spec: spec, manager: manager}, nil + return &EnvAddCmd{name: name, server: server, namespace: namespace, spec: specFlag, manager: manager}, nil } func (c *EnvAddCmd) Run() error { diff --git a/pkg/kubecfg/init.go b/pkg/kubecfg/init.go index f6bf91647d965136337e512746a0cba8f3dccc18..f61ddea0b07c7d944492feccb5dc74c4ec00a5d7 100644 --- a/pkg/kubecfg/init.go +++ b/pkg/kubecfg/init.go @@ -6,27 +6,19 @@ import ( ) type InitCmd struct { - name string - rootPath string - spec metadata.ClusterSpec - serverURI *string - namespace *string + name string + rootPath string + k8sSpecFlag *string + serverURI *string + namespace *string } -func NewInitCmd(name, rootPath, specFlag string, serverURI, namespace *string) (*InitCmd, error) { - // NOTE: We're taking `rootPath` here as an absolute path (rather than a partial path we expand to an absolute path) - // to make it more testable. - - spec, err := metadata.ParseClusterSpec(specFlag) - if err != nil { - return nil, err - } - - return &InitCmd{name: name, rootPath: rootPath, spec: spec, serverURI: serverURI, namespace: namespace}, nil +func NewInitCmd(name, rootPath string, k8sSpecFlag, serverURI, namespace *string) (*InitCmd, error) { + return &InitCmd{name: name, rootPath: rootPath, k8sSpecFlag: k8sSpecFlag, serverURI: serverURI, namespace: namespace}, nil } func (c *InitCmd) Run() error { - _, err := metadata.Init(c.name, c.rootPath, c.spec, c.serverURI, c.namespace) + _, err := metadata.Init(c.name, c.rootPath, c.k8sSpecFlag, c.serverURI, c.namespace) if err == nil { log.Info("ksonnet app successfully created! Next, try creating a component with `ks generate`.") } diff --git a/testdata/testapp/app.yaml b/testdata/testapp/app.yaml index 2240d9f397ac0e313c04df9a3ee9cdf8fecf1912..0f8e9878e3d922653b3b389135316695a5e6af87 100644 --- a/testdata/testapp/app.yaml +++ b/testdata/testapp/app.yaml @@ -13,6 +13,6 @@ environments: destinations: - namespace: foo server: foo - k8sVersion: "1.8.1" + k8sVersion: "v1.8.1" path: default version: 0.0.1 diff --git a/testdata/testapp/lib/v1.8.1/.keep b/testdata/testapp/lib/v1.8.1/.keep new file mode 100644 index 0000000000000000000000000000000000000000..9470d4088dfef16c4eeb7d5e845e86a63e78627a --- /dev/null +++ b/testdata/testapp/lib/v1.8.1/.keep @@ -0,0 +1,4 @@ +Git will not check in empty directories. + +This directory is needed for testing ks project structure. This file exists so +that its parent directory can be checked in.