Skip to content
Snippets Groups Projects
Commit 4664eaa6 authored by Alex Clemmer's avatar Alex Clemmer
Browse files

Implement ClusterSpec.data for `version:` scheme

`metadata.ClusterSpec` represents a specification for an abstract
Kubernetes cluster. For example, `version:1.7.0` represents a Kubernetes
cluster running a build from 1.7.0. This specification is primarily used
to generate ksonnet-lib.

This struct exposes a method, `data` that will retrieve the OpenAPI JSON
that specifies the API for a Kubernetes cluster. Eventually, `data` will
be able to read a file, pull from a URL, pull from a live cluster, or
pull a specific version of the API from the official Kubernetes
repository.

This commit introduces the the last of these options.
parent ccd74079
No related branches found
No related tags found
No related merge requests found
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 {
......
......@@ -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 {
......
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()
}
......@@ -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())
}
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment