diff --git a/metadata/clusterspec.go b/metadata/clusterspec.go index bbe44dd15bdc584da78dcd062f2cc11441ee948e..a3c3f02b56c595cc19d5b17aedc400072986f1f0 100644 --- a/metadata/clusterspec.go +++ b/metadata/clusterspec.go @@ -1,15 +1,49 @@ 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": + abs, err := filepath.Abs(split[1]) + if err != nil { + return nil, err + } + absPath := AbsPath(abs) + return &clusterSpecFile{specPath: absPath, 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 AbsPath + fs afero.Fs } func (cs *clusterSpecFile) data() ([]byte, error) { - return afero.ReadFile(appFS, string(cs.specPath)) + return afero.ReadFile(cs.fs, string(cs.specPath)) } func (cs *clusterSpecFile) resource() string { @@ -21,8 +55,7 @@ type clusterSpecLive struct { } func (cs *clusterSpecLive) data() ([]byte, error) { - // TODO: Implement getting spec from path, k8sVersion, and URL. - panic("Not implemented") + return nil, fmt.Errorf("Initializing from OpenAPI spec in live cluster is not implemented") } func (cs *clusterSpecLive) resource() string { @@ -34,8 +67,20 @@ type clusterSpecVersion struct { } func (cs *clusterSpecVersion) data() ([]byte, error) { - // TODO: Implement getting spec from path, k8sVersion, and URL. - panic("Not implemented") + 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 { diff --git a/metadata/clusterspec_test.go b/metadata/clusterspec_test.go index 4d13beda69ff703293c4cf29e4546c30a51aeccf..3a4f28f896a8e4e04d196ede6efe1bf14c65fad9 100644 --- a/metadata/clusterspec_test.go +++ b/metadata/clusterspec_test.go @@ -12,13 +12,13 @@ type parseSuccess struct { var successTests = []parseSuccess{ {"version:v1.7.1", &clusterSpecVersion{"v1.7.1"}}, - {"file:swagger.json", &clusterSpecFile{"swagger.json"}}, + {"file:swagger.json", &clusterSpecFile{"swagger.json", testFS}}, {"url:file:///some_file", &clusterSpecLive{"file:///some_file"}}, } func TestClusterSpecParsingSuccess(t *testing.T) { for _, test := range successTests { - parsed, err := ParseClusterSpec(test.input) + parsed, err := parseClusterSpec(test.input, testFS) if err != nil { t.Errorf("Failed to parse spec: %v", err) } @@ -66,7 +66,7 @@ var failureTests = []parseFailure{ func TestClusterSpecParsingFailure(t *testing.T) { for _, test := range failureTests { - _, err := ParseClusterSpec(test.input) + _, 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/interface.go b/metadata/interface.go index 5d9ed9e39458da38ff544adde7b4e9c890df77ba..bfa05ea8fc856f788d3d05f6ee1dea8965311e94 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -1,13 +1,11 @@ package metadata import ( - "fmt" - "path/filepath" - "strings" - "github.com/spf13/afero" ) +var appFS afero.Fs + // AbsPath is an advisory type that represents an absolute path. It is advisory // in that it is not forced to be absolute, but rather, meant to indicate // intent, and make code easier to read. @@ -40,7 +38,7 @@ func Find(path AbsPath) (Manager, error) { // capabilities-compliant version of ksonnet-lib, and then generate the // directory tree for an application. func Init(rootPath AbsPath, spec ClusterSpec) (Manager, error) { - return initManager(rootPath, spec, afero.NewOsFs()) + return initManager(rootPath, spec, appFS) } // ClusterSpec represents the API supported by some cluster. There are several @@ -57,24 +55,9 @@ type ClusterSpec interface { // will output a ClusterSpec representing the cluster specification associated // with the `v1.7.1` build of Kubernetes. func ParseClusterSpec(specFlag string) (ClusterSpec, error) { - split := strings.SplitN(specFlag, ":", 2) - if len(split) == 0 || len(split) == 1 || split[1] == "" { - return nil, fmt.Errorf("Invalid API specification '%s'", specFlag) - } + return parseClusterSpec(specFlag, appFS) +} - switch split[0] { - case "version": - return &clusterSpecVersion{k8sVersion: split[1]}, nil - case "file": - abs, err := filepath.Abs(split[1]) - if err != nil { - return nil, err - } - absPath := AbsPath(abs) - return &clusterSpecFile{specPath: absPath}, nil - case "url": - return &clusterSpecLive{apiServerURL: split[1]}, nil - default: - return nil, fmt.Errorf("Could not parse cluster spec '%s'", specFlag) - } +func init() { + appFS = afero.NewOsFs() } diff --git a/metadata/manager_test.go b/metadata/manager_test.go index d73fd97cc8c4f1e8bb332a3d35c9d40e680b70ee..5dabda9ff14d45333d6c4cad62039c7dd560061a 100644 --- a/metadata/manager_test.go +++ b/metadata/manager_test.go @@ -31,20 +31,20 @@ const ( ` ) -var appFS = afero.NewMemMapFs() +var testFS = afero.NewMemMapFs() func init() { - afero.WriteFile(appFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm) + afero.WriteFile(testFS, blankSwagger, []byte(blankSwaggerData), os.ModePerm) } func TestInitSuccess(t *testing.T) { - spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger)) + spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) if err != nil { t.Fatalf("Failed to parse cluster spec: %v", err) } appPath := AbsPath("/fromEmptySwagger") - _, err = initManager(appPath, spec, appFS) + _, err = initManager(appPath, spec, testFS) if err != nil { t.Fatalf("Failed to init cluster spec: %v", err) } @@ -62,7 +62,7 @@ func TestInitSuccess(t *testing.T) { for _, p := range paths { path := appendToAbsPath(appPath, string(p)) - exists, err := afero.DirExists(appFS, string(path)) + exists, err := afero.DirExists(testFS, string(path)) if err != nil { t.Fatalf("Expected to create directory '%s', but failed:\n%v", p, err) } else if !exists { @@ -72,7 +72,7 @@ func TestInitSuccess(t *testing.T) { envPath := appendToAbsPath(appPath, string(defaultEnvDir)) schemaPath := appendToAbsPath(envPath, schemaFilename) - bytes, err := afero.ReadFile(appFS, string(schemaPath)) + 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 { @@ -80,7 +80,7 @@ func TestInitSuccess(t *testing.T) { } ksonnetLibPath := appendToAbsPath(envPath, ksonnetLibCoreFilename) - ksonnetLibBytes, err := afero.ReadFile(appFS, string(ksonnetLibPath)) + ksonnetLibBytes, err := afero.ReadFile(testFS, string(ksonnetLibPath)) if err != nil { t.Fatalf("Failed to read ksonnet-lib file at '%s':\n%v", ksonnetLibPath, err) } else if actualKsonnetLib := string(ksonnetLibBytes); actualKsonnetLib != blankKsonnetLib { @@ -90,7 +90,7 @@ func TestInitSuccess(t *testing.T) { func TestFindSuccess(t *testing.T) { findSuccess := func(t *testing.T, appDir, currDir AbsPath) { - m, err := findManager(currDir, appFS) + m, err := findManager(currDir, testFS) if err != nil { t.Fatalf("Failed to find manager at path '%s':\n%v", currDir, err) } else if m.rootPath != appDir { @@ -98,13 +98,13 @@ func TestFindSuccess(t *testing.T) { } } - spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger)) + spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) if err != nil { t.Fatalf("Failed to parse cluster spec: %v", err) } appPath := AbsPath("/findSuccess") - _, err = initManager(appPath, spec, appFS) + _, err = initManager(appPath, spec, testFS) if err != nil { t.Fatalf("Failed to init cluster spec: %v", err) } @@ -116,7 +116,7 @@ func TestFindSuccess(t *testing.T) { // Create empty app file. appFile := appendToAbsPath(components, "app.jsonnet") - f, err := appFS.OpenFile(string(appFile), os.O_RDONLY|os.O_CREATE, 0777) + f, err := testFS.OpenFile(string(appFile), os.O_RDONLY|os.O_CREATE, 0777) if err != nil { t.Fatalf("Failed to touch app file '%s'\n%v", appFile, err) } @@ -127,7 +127,7 @@ func TestFindSuccess(t *testing.T) { func TestFindFailure(t *testing.T) { findFailure := func(t *testing.T, currDir AbsPath) { - _, err := findManager(currDir, appFS) + _, err := findManager(currDir, testFS) if err == nil { t.Fatalf("Expected to fail to find ksonnet app in '%s', but succeeded", currDir) } @@ -139,20 +139,20 @@ func TestFindFailure(t *testing.T) { } func TestDoubleNewFailure(t *testing.T) { - spec, err := ParseClusterSpec(fmt.Sprintf("file:%s", blankSwagger)) + spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS) if err != nil { t.Fatalf("Failed to parse cluster spec: %v", err) } appPath := AbsPath("/doubleNew") - _, err = initManager(appPath, spec, appFS) + _, err = initManager(appPath, spec, 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(appPath, spec, appFS) + _, err = initManager(appPath, spec, testFS) if err == nil || err.Error() != targetErr { t.Fatalf("Expected to fail to create app with message '%s', got '%s'", targetErr, err.Error()) }