Skip to content
Snippets Groups Projects
Commit 651c24cd authored by Alex Clemmer's avatar Alex Clemmer Committed by GitHub
Browse files

Merge pull request #88 from hausdorff/ks-gen

Implement `init`; bring synchronization primitives to spec
parents b4883e32 af3f0f6c
No related branches found
No related tags found
No related merge requests found
......@@ -2,7 +2,6 @@ language: go
go:
- 1.8.x
- 1.7.x
os:
- linux
......
......@@ -20,7 +20,9 @@ EXTRA_GO_FLAGS =
GO_FLAGS = -ldflags="-X main.version=$(VERSION) $(GO_LDFLAGS)" $(EXTRA_GO_FLAGS)
GOFMT = gofmt
JSONNET_FILES = lib/kubecfg_test.jsonnet examples/guestbook.jsonnet
KCFG_TEST_FILE = lib/kubecfg_test.jsonnet
GUESTBOOK_FILE = examples/guestbook.jsonnet
JSONNET_FILES = $(KCFG_TEST_FILE) $(GUESTBOOK_FILE)
# TODO: Simplify this once ./... ignores ./vendor
GO_PACKAGES = ./cmd/... ./utils/... ./pkg/... ./metadata/...
......@@ -36,7 +38,7 @@ gotest:
jsonnettest: kubecfg $(JSONNET_FILES)
# TODO: use `kubecfg check` once implemented
./kubecfg -J lib show $(JSONNET_FILES) >/dev/null
./kubecfg -J lib show -f $(KCFG_TEST_FILE) -f $(GUESTBOOK_FILE) >/dev/null
vet:
$(GO) vet $(GO_FLAGS) $(GO_PACKAGES)
......
......@@ -33,6 +33,7 @@ const (
func init() {
RootCmd.AddCommand(deleteCmd)
addEnvCmdFlags(deleteCmd)
deleteCmd.PersistentFlags().Int64(flagGracePeriod, -1, "Number of seconds given to resources to terminate gracefully. A negative value is ignored")
}
......@@ -47,7 +48,17 @@ var deleteCmd = &cobra.Command{
return err
}
objs, err := readObjs(cmd, args)
files, err := getFiles(cmd, args)
if err != nil {
return err
}
vm, err := newExpander(cmd)
if err != nil {
return err
}
objs, err := vm.Expand(files)
if err != nil {
return err
}
......
......@@ -36,23 +36,33 @@ const flagDiffStrategy = "diff-strategy"
var ErrDiffFound = fmt.Errorf("Differences found.")
func init() {
addEnvCmdFlags(diffCmd)
diffCmd.PersistentFlags().String(flagDiffStrategy, "all", "Diff strategy, all or subset.")
RootCmd.AddCommand(diffCmd)
}
var diffCmd = &cobra.Command{
Use: "diff",
Use: "diff [<env>|-f <file-or-dir>]",
Short: "Display differences between server and local config",
RunE: func(cmd *cobra.Command, args []string) error {
out := cmd.OutOrStdout()
flags := cmd.Flags()
diffStrategy, err := flags.GetString(flagDiffStrategy)
if err != nil {
return err
}
objs, err := readObjs(cmd, args)
files, err := getFiles(cmd, args)
if err != nil {
return err
}
vm, err := newExpander(cmd)
if err != nil {
return err
}
objs, err := vm.Expand(files)
if err != nil {
return err
}
......@@ -122,6 +132,22 @@ var diffCmd = &cobra.Command{
}
return nil
},
Long: `Display differences between server and local configuration.
ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet
files.`,
Example: ` # Show diff between resources described in a local ksonnet application and
# the cluster referenced by the 'dev' environment. Can be used in any
# subdirectory of the application.
ksonnet diff -e=dev
# Show diff between resources described in a YAML file and the cluster
# referenced in '$KUBECONFIG'.
ksonnet diff -f ./pod.yaml
# Show diff between resources described in a YAML file and the cluster
# referred to by './kubeconfig'.
ksonnet diff --kubeconfig=./kubeconfig -f ./pod.yaml`,
}
func removeFields(config, live interface{}) interface{} {
......
// 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"
"path"
"path/filepath"
"github.com/ksonnet/kubecfg/metadata"
"github.com/ksonnet/kubecfg/pkg/kubecfg"
"github.com/spf13/cobra"
)
const (
flagAPISpec = "api-spec"
)
func init() {
RootCmd.AddCommand(initCmd)
// TODO: We need to make this default to checking the `kubeconfig` file.
initCmd.PersistentFlags().String(flagAPISpec, "version:v1.7.0", "Manually specify API version from OpenAPI schema, cluster, or Kubernetes version")
}
var initCmd = &cobra.Command{
Use: "init <app-name>",
Short: "Initialize a ksonnet project",
RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
if len(args) != 1 {
return fmt.Errorf("'init' takes a single argument that names the application we're initializing")
}
appName := args[0]
appDir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return err
}
appRoot := metadata.AbsPath(path.Join(appDir, appName))
specFlag, err := flags.GetString(flagAPISpec)
if err != nil {
return err
}
c, err := kubecfg.NewInitCmd(appRoot, specFlag)
if err != nil {
return err
}
return c.Run()
},
Long: `Initialize a ksonnet project in a new directory, 'app-name'. This process
consists of two steps:
1. Generating ksonnet-lib. Users can set flags to generate the library based on
a variety of data, including server configuration and an OpenAPI
specification of a Kubernetes build. By default, this is generated from the
capabilities of the cluster specified in the cluster of the current context
specified in $KUBECONFIG.
2. Generating the following tree in the current directory.
app-name/
.gitignore Default .gitignore; can customize VCS
.ksonnet/ Metadata for ksonnet
envs/ Env specs (defaults: dev, test, prod)
params.yaml Specifies the schema of the environments
dev.yaml
test.yaml
prod.yaml
us-east.yaml [Example of user-generated env]
components/ Top-level Kubernetes objects defining application
lib/ user-written .libsonnet files
vendor/ mixin libraries, prototypes
`,
Example: ` # Initialize ksonnet application, using the capabilities of live cluster
# specified in the $KUBECONFIG environment variable (specifically: the
# current context) to generate 'ksonnet-lib'.
ksonnet init app-name
# Initialize ksonnet application, using the OpenAPI specification generated
# in the Kubenetes v1.7.1 build to generate 'ksonnet-lib'.
ksonnet init app-name --api-spec=version:v1.7.1
# Initialize ksonnet application, using an OpenAPI specification file
# generated by a build of Kubernetes to generate 'ksonnet-lib'.
ksonnet init app-name --api-spec=file:swagger.json`,
}
......@@ -21,21 +21,20 @@ import (
goflag "flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
jsonnet "github.com/strickyak/jsonnet_cgo"
"golang.org/x/crypto/ssh/terminal"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/tools/clientcmd"
"github.com/ksonnet/kubecfg/metadata"
"github.com/ksonnet/kubecfg/pkg/kubecfg"
"github.com/ksonnet/kubecfg/template"
"github.com/ksonnet/kubecfg/utils"
// Register auth plugins
......@@ -51,6 +50,11 @@ const (
flagTlaVarFile = "tla-str-file"
flagResolver = "resolve-images"
flagResolvFail = "resolve-images-error"
// For use in the commands (e.g., diff, update, delete) that require either an
// environment or the -f flag.
flagFile = "file"
flagFileShort = "f"
)
var clientConfig clientcmd.ClientConfig
......@@ -155,176 +159,49 @@ func (f *logFormatter) Format(e *log.Entry) ([]byte, error) {
return buf.Bytes(), nil
}
// JsonnetVM constructs a new jsonnet.VM, according to command line
// flags
func JsonnetVM(cmd *cobra.Command) (*jsonnet.VM, error) {
vm := jsonnet.Make()
func newExpander(cmd *cobra.Command) (*template.Expander, error) {
flags := cmd.Flags()
spec := template.Expander{}
var err error
jpath := os.Getenv("KUBECFG_JPATH")
for _, p := range filepath.SplitList(jpath) {
log.Debugln("Adding jsonnet search path", p)
vm.JpathAdd(p)
}
spec.EnvJPath = filepath.SplitList(os.Getenv("KUBECFG_JPATH"))
jpath, err := flags.GetString(flagJpath)
if err != nil {
return nil, err
}
for _, p := range filepath.SplitList(jpath) {
log.Debugln("Adding jsonnet search path", p)
vm.JpathAdd(p)
}
spec.FlagJpath = filepath.SplitList(jpath)
extvars, err := flags.GetStringSlice(flagExtVar)
spec.ExtVars, err = flags.GetStringSlice(flagExtVar)
if err != nil {
return nil, err
}
for _, extvar := range extvars {
kv := strings.SplitN(extvar, "=", 2)
switch len(kv) {
case 1:
v, present := os.LookupEnv(kv[0])
if present {
vm.ExtVar(kv[0], v)
} else {
return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
}
case 2:
vm.ExtVar(kv[0], kv[1])
}
}
extvarfiles, err := flags.GetStringSlice(flagExtVarFile)
spec.ExtVarFiles, err = flags.GetStringSlice(flagExtVarFile)
if err != nil {
return nil, err
}
for _, extvar := range extvarfiles {
kv := strings.SplitN(extvar, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("Failed to parse %s: missing '=' in %s", flagExtVarFile, extvar)
}
v, err := ioutil.ReadFile(kv[1])
if err != nil {
return nil, err
}
vm.ExtVar(kv[0], string(v))
}
tlavars, err := flags.GetStringSlice(flagTlaVar)
spec.TlaVars, err = flags.GetStringSlice(flagTlaVar)
if err != nil {
return nil, err
}
for _, tlavar := range tlavars {
kv := strings.SplitN(tlavar, "=", 2)
switch len(kv) {
case 1:
v, present := os.LookupEnv(kv[0])
if present {
vm.TlaVar(kv[0], v)
} else {
return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
}
case 2:
vm.TlaVar(kv[0], kv[1])
}
}
tlavarfiles, err := flags.GetStringSlice(flagTlaVarFile)
if err != nil {
return nil, err
}
for _, tlavar := range tlavarfiles {
kv := strings.SplitN(tlavar, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("Failed to parse %s: missing '=' in %s", flagTlaVarFile, tlavar)
}
v, err := ioutil.ReadFile(kv[1])
if err != nil {
return nil, err
}
vm.TlaVar(kv[0], string(v))
}
resolver, err := buildResolver(cmd)
spec.TlaVarFiles, err = flags.GetStringSlice(flagTlaVarFile)
if err != nil {
return nil, err
}
utils.RegisterNativeFuncs(vm, resolver)
return vm, nil
}
func buildResolver(cmd *cobra.Command) (utils.Resolver, error) {
flags := cmd.Flags()
resolver, err := flags.GetString(flagResolver)
spec.Resolver, err = flags.GetString(flagResolver)
if err != nil {
return nil, err
}
failAction, err := flags.GetString(flagResolvFail)
spec.FailAction, err = flags.GetString(flagResolvFail)
if err != nil {
return nil, err
}
ret := resolverErrorWrapper{}
switch failAction {
case "ignore":
ret.OnErr = func(error) error { return nil }
case "warn":
ret.OnErr = func(err error) error {
log.Warning(err.Error())
return nil
}
case "error":
ret.OnErr = func(err error) error { return err }
default:
return nil, fmt.Errorf("Bad value for --%s: %s", flagResolvFail, failAction)
}
switch resolver {
case "noop":
ret.Inner = utils.NewIdentityResolver()
case "registry":
ret.Inner = utils.NewRegistryResolver(&http.Client{
Transport: utils.NewAuthTransport(http.DefaultTransport),
})
default:
return nil, fmt.Errorf("Bad value for --%s: %s", flagResolver, resolver)
}
return &ret, nil
}
type resolverErrorWrapper struct {
Inner utils.Resolver
OnErr func(error) error
}
func (r *resolverErrorWrapper) Resolve(image *utils.ImageName) error {
err := r.Inner.Resolve(image)
if err != nil {
err = r.OnErr(err)
}
return err
}
func readObjs(cmd *cobra.Command, paths []string) ([]*unstructured.Unstructured, error) {
vm, err := JsonnetVM(cmd)
if err != nil {
return nil, err
}
defer vm.Destroy()
res := []*unstructured.Unstructured{}
for _, path := range paths {
objs, err := utils.Read(vm, path)
if err != nil {
return nil, fmt.Errorf("Error reading %s: %v", path, err)
}
res = append(res, utils.FlattenToV1(objs)...)
}
return res, nil
return &spec, nil
}
// For debugging
......@@ -356,3 +233,39 @@ func restClientPool(cmd *cobra.Command) (dynamic.ClientPool, discovery.Discovery
pool := dynamic.NewClientPool(conf, mapper, pathresolver)
return pool, discoCache, nil
}
func addEnvCmdFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringArrayP(flagFile, flagFileShort, nil, "Filename or directory that contains the configuration to apply (accepts YAML, JSON, and Jsonnet)")
}
func parseEnvCmd(cmd *cobra.Command, args []string) (*string, []string, error) {
flags := cmd.Flags()
files, err := flags.GetStringArray(flagFile)
if err != nil {
return nil, nil, err
}
var env *string
if len(args) == 1 {
env = &args[0]
}
return env, files, nil
}
// TODO: Remove this and use `kubecfg.GetFiles` when we move commands into
// `pkg`.
func getFiles(cmd *cobra.Command, args []string) ([]string, error) {
env, files, err := parseEnvCmd(cmd, args)
if err != nil {
return nil, err
}
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
return kubecfg.GetFiles(metadata.AbsPath(cwd), env, files)
}
......@@ -29,17 +29,28 @@ const (
func init() {
RootCmd.AddCommand(showCmd)
addEnvCmdFlags(showCmd)
showCmd.PersistentFlags().StringP(flagFormat, "o", "yaml", "Output format. Supported values are: json, yaml")
}
var showCmd = &cobra.Command{
Use: "show",
Use: "show [<env>|-f <file-or-dir>]",
Short: "Show expanded resource definitions",
RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
out := cmd.OutOrStdout()
objs, err := readObjs(cmd, args)
files, err := getFiles(cmd, args)
if err != nil {
return err
}
vm, err := newExpander(cmd)
if err != nil {
return err
}
objs, err := vm.Expand(files)
if err != nil {
return err
}
......
......@@ -81,7 +81,7 @@ func TestShow(t *testing.T) {
output := cmdOutput(t, []string{"show",
"-J", filepath.FromSlash("../testdata/lib"),
"-o", format,
filepath.FromSlash("../testdata/test.jsonnet"),
"-f", filepath.FromSlash("../testdata/test.jsonnet"),
"-V", "aVar=aVal",
"-V", "anVar",
"--ext-str-file", "filevar=" + filepath.FromSlash("../testdata/extvar.file"),
......
......@@ -16,8 +16,11 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
"github.com/ksonnet/kubecfg/metadata"
"github.com/ksonnet/kubecfg/pkg/kubecfg"
)
......@@ -45,6 +48,8 @@ const (
func init() {
RootCmd.AddCommand(updateCmd)
addEnvCmdFlags(updateCmd)
updateCmd.PersistentFlags().Bool(flagCreate, true, "Create missing resources")
updateCmd.PersistentFlags().Bool(flagSkipGc, false, "Don't perform garbage collection, even with --"+flagGcTag)
updateCmd.PersistentFlags().String(flagGcTag, "", "Add this tag to updated objects, and garbage collect existing objects with this tag and not in config")
......@@ -52,13 +57,20 @@ func init() {
}
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update Kubernetes resources with local config",
Use: "update [<env>|-f <file-or-dir>]",
Short: `Update (or optionally create) Kubernetes resources on the cluster using the
local configuration. Accepts JSON, YAML, or Jsonnet.`,
RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
var err error
c := kubecfg.UpdateCmd{}
c.Environment, c.Files, err = parseEnvCmd(cmd, args)
if err != nil {
return err
}
c.Create, err = flags.GetBool(flagCreate)
if err != nil {
return err
......@@ -89,11 +101,37 @@ var updateCmd = &cobra.Command{
return err
}
objs, err := readObjs(cmd, args)
c.Expander, err = newExpander(cmd)
if err != nil {
return err
}
cwd, err := os.Getwd()
if err != nil {
return err
}
return c.Run(objs)
return c.Run(metadata.AbsPath(cwd))
},
Long: `Update (or optionally create) Kubernetes resources on the cluster using the
local configuration. Use the '--create' flag to control whether we create them
if they do not exist (default: true).
ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet
files.`,
Example: ` # Create or update all resources described in a ksonnet application, and
# running in the 'dev' environment. Can be used in any subdirectory of the
# application.
ksonnet update dev
# Create or update resources described in a YAML file. Automatically picks up
# the cluster's location from '$KUBECONFIG'.
ksonnet appy -f ./pod.yaml
# Update resources described in a YAML file, and running in cluster referred
# to by './kubeconfig'.
ksonnet update --kubeconfig=./kubeconfig -f ./pod.yaml
# Display set of actions we will execute when we run 'update'.
ksonnet update dev --dry-run`,
}
......@@ -26,16 +26,28 @@ import (
func init() {
RootCmd.AddCommand(validateCmd)
addEnvCmdFlags(validateCmd)
}
var validateCmd = &cobra.Command{
Use: "validate",
Use: "validate [<env>|-f <file-or-dir>]",
Short: "Compare generated manifest against server OpenAPI spec",
RunE: func(cmd *cobra.Command, args []string) error {
objs, err := readObjs(cmd, args)
files, err := getFiles(cmd, args)
if err != nil {
return err
}
vm, err := newExpander(cmd)
if err != nil {
return err
}
objs, err := vm.Expand(files)
if err != nil {
return err
}
_, disco, err := restClientPool(cmd)
if err != nil {
return err
......@@ -69,4 +81,20 @@ var validateCmd = &cobra.Command{
return nil
},
Long: `Validate that an application or file is compliant with the Kubernetes
specification.
ksonnet applications are accepted, as well as normal JSON, YAML, and Jsonnet
files.`,
Example: ` # Validate all resources described in a ksonnet application, expanding
# ksonnet code with 'dev' environment where necessary (i.e., not YAML, JSON,
# or non-ksonnet Jsonnet code).
ksonnet validate -e=dev
# Validate resources described in a YAML file.
ksonnet validate -f ./pod.yaml
# Validate resources described in a Jsonnet file. Does not expand using
# environment bindings.
ksonnet validate -f ./pod.jsonnet`,
}
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.
type AbsPath string
// AbsPaths is a slice of `AbsPath`.
type AbsPaths []string
// Manager abstracts over a ksonnet application's metadata, allowing users to do
// things like: create and delete environments; search for prototypes; vendor
// libraries; and other non-core-application tasks.
type Manager interface {
Root() AbsPath
ComponentPaths() (AbsPaths, error)
//
// TODO: Fill in methods as we need them.
//
......@@ -40,7 +42,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 +59,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()
}
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"
)
......@@ -24,7 +27,8 @@ const (
schemaDir = "vendor/schema"
vendorLibDir = "vendor/lib"
schemaFilename = "swagger.json"
schemaFilename = "swagger.json"
ksonnetLibCoreFilename = "k8s.libsonnet"
)
type manager struct {
......@@ -62,18 +66,41 @@ func findManager(abs AbsPath, appFS afero.Fs) (*manager, error) {
}
func initManager(rootPath AbsPath, spec ClusterSpec, appFS afero.Fs) (*manager, error) {
data, err := spec.data()
//
// 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.
//
// 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.
ksonnetLibDir := appendToAbsPath(m.schemaDir, defaultEnvName)
ksonnetLibData, err := generateKsonnetLibData(ksonnetLibDir, specData)
if err != nil {
return nil, err
}
// Initialize directory structure.
if err = m.createAppDirTree(); err != nil {
return nil, err
}
if err = m.cacheClusterSpecData(defaultEnvName, data); err != nil {
// Cache specification data.
if err = m.cacheClusterSpecData(defaultEnvName, specData); err != nil {
return nil, err
}
ksonnetLibPath := appendToAbsPath(ksonnetLibDir, ksonnetLibCoreFilename)
err = afero.WriteFile(appFS, string(ksonnetLibPath), ksonnetLibData, 0644)
if err != nil {
return nil, err
}
......@@ -98,6 +125,25 @@ func (m *manager) Root() AbsPath {
return m.rootPath
}
func (m *manager) ComponentPaths() (AbsPaths, error) {
paths := []string{}
err := afero.Walk(m.appFS, string(m.componentsPath), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
paths = append(paths, path)
}
return nil
})
if err != nil {
return nil, err
}
return paths, nil
}
func (m *manager) cacheClusterSpecData(name string, specData []byte) error {
envPath := string(appendToAbsPath(m.schemaDir, name))
err := m.appFS.MkdirAll(envPath, os.ModePerm)
......@@ -136,3 +182,18 @@ func (m *manager) createAppDirTree() error {
return nil
}
func generateKsonnetLibData(ksonnetLibDir AbsPath, text []byte) ([]byte, error) {
// Deserialize the API object.
s := kubespec.APISpec{}
err := json.Unmarshal(text, &s)
if err != nil {
return nil, err
}
s.Text = text
s.FilePath = filepath.Dir(string(ksonnetLibDir))
// Emit Jsonnet code.
return ksonnet.Emit(&s, nil, nil)
}
......@@ -3,6 +3,7 @@ package metadata
import (
"fmt"
"os"
"sort"
"testing"
"github.com/spf13/afero"
......@@ -10,23 +11,41 @@ import (
const (
blankSwagger = "/blankSwagger.json"
blankSwaggerData = `{}`
blankSwaggerData = `{
"swagger": "2.0",
"info": {
"title": "Kubernetes",
"version": "v1.7.0"
},
"paths": {
},
"definitions": {
}
}`
blankKsonnetLib = `// AUTOGENERATED from the Kubernetes OpenAPI specification. DO NOT MODIFY.
// Kubernetes version: v1.7.0
{
local hidden = {
},
}
`
)
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)
}
......@@ -44,7 +63,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 {
......@@ -54,17 +73,25 @@ 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 {
t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", schemaPath, blankSwaggerData, actualSwagger)
}
ksonnetLibPath := appendToAbsPath(envPath, ksonnetLibCoreFilename)
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 {
t.Fatalf("Expected swagger file at '%s' to have value: '%s', got: '%s'", ksonnetLibPath, blankKsonnetLib, actualKsonnetLib)
}
}
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 {
......@@ -72,13 +99,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)
}
......@@ -90,7 +117,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)
}
......@@ -99,9 +126,62 @@ func TestFindSuccess(t *testing.T) {
findSuccess(t, appPath, appFile)
}
func TestComponentPaths(t *testing.T) {
spec, err := parseClusterSpec(fmt.Sprintf("file:%s", blankSwagger), testFS)
if err != nil {
t.Fatalf("Failed to parse cluster spec: %v", err)
}
appPath := AbsPath("/componentPaths")
m, err := initManager(appPath, spec, testFS)
if err != nil {
t.Fatalf("Failed to init cluster spec: %v", err)
}
// Create empty app file.
components := appendToAbsPath(appPath, componentsDir)
appFile1 := appendToAbsPath(components, "component1.jsonnet")
f1, err := testFS.OpenFile(string(appFile1), os.O_RDONLY|os.O_CREATE, 0777)
if err != nil {
t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
}
f1.Close()
// Create empty file in a nested directory.
appSubdir := appendToAbsPath(components, "appSubdir")
err = testFS.MkdirAll(string(appSubdir), os.ModePerm)
if err != nil {
t.Fatalf("Failed to create directory '%s'\n%v", appSubdir, err)
}
appFile2 := appendToAbsPath(appSubdir, "component2.jsonnet")
f2, err := testFS.OpenFile(string(appFile2), os.O_RDONLY|os.O_CREATE, 0777)
if err != nil {
t.Fatalf("Failed to touch app file '%s'\n%v", appFile1, err)
}
f2.Close()
// Create a directory that won't be listed in the call to `ComponentPaths`.
unlistedDir := string(appendToAbsPath(components, "doNotListMe"))
err = testFS.MkdirAll(unlistedDir, os.ModePerm)
if err != nil {
t.Fatalf("Failed to create directory '%s'\n%v", unlistedDir, err)
}
paths, err := m.ComponentPaths()
if err != nil {
t.Fatalf("Failed to find component paths: %v", err)
}
sort.Slice(paths, func(i, j int) bool { return paths[i] < paths[j] })
if len(paths) != 2 || paths[0] != string(appFile2) || paths[1] != string(appFile1) {
t.Fatalf("m.ComponentPaths failed; expected '%s', got '%s'", []string{string(appFile1), string(appFile2)}, paths)
}
}
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)
}
......@@ -113,20 +193,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())
}
......
package kubecfg
import (
"fmt"
"github.com/ksonnet/kubecfg/metadata"
)
// TODO: Make this private when we move more commands into `pkg`.
func GetFiles(wd metadata.AbsPath, env *string, files []string) ([]string, error) {
envPresent := env != nil
filesPresent := len(files) > 0
// This is equivalent to: `if !xor(envPresent, filesPresent) {`
if envPresent && filesPresent {
return nil, fmt.Errorf("Either an environment name or a file list is required, but not both")
} else if !envPresent && !filesPresent {
return nil, fmt.Errorf("Must specify either an environment or a file list")
}
if envPresent {
manager, err := metadata.Find(wd)
if err != nil {
return nil, err
}
files, err = manager.ComponentPaths()
if err != nil {
return nil, err
}
}
return files, nil
}
package kubecfg
import "github.com/ksonnet/kubecfg/metadata"
type InitCmd struct {
rootPath metadata.AbsPath
spec metadata.ClusterSpec
}
func NewInitCmd(rootPath metadata.AbsPath, specFlag 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{rootPath: rootPath, spec: spec}, nil
}
func (c *InitCmd) Run() error {
_, err := metadata.Init(c.rootPath, c.spec)
return err
}
......@@ -9,7 +9,6 @@ import (
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
......@@ -18,6 +17,8 @@ import (
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"github.com/ksonnet/kubecfg/metadata"
"github.com/ksonnet/kubecfg/template"
"github.com/ksonnet/kubecfg/utils"
)
......@@ -44,13 +45,27 @@ type UpdateCmd struct {
Discovery discovery.DiscoveryInterface
DefaultNamespace string
Expander *template.Expander
Environment *string
Files []string
Create bool
GcTag string
SkipGc bool
DryRun bool
}
func (c UpdateCmd) Run(objs []*unstructured.Unstructured) error {
func (c UpdateCmd) Run(wd metadata.AbsPath) error {
files, err := GetFiles(wd, c.Environment, c.Files)
if err != nil {
return err
}
objs, err := c.Expander.Expand(files)
if err != nil {
return err
}
dryRunText := ""
if c.DryRun {
dryRunText = " (dry-run)"
......
package template
import (
"fmt"
"io/ioutil"
"os"
"strings"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/ksonnet/kubecfg/utils"
log "github.com/sirupsen/logrus"
jsonnet "github.com/strickyak/jsonnet_cgo"
)
type Expander struct {
EnvJPath []string
FlagJpath []string
ExtVars []string
ExtVarFiles []string
TlaVars []string
TlaVarFiles []string
Resolver string
FailAction string
}
func (spec *Expander) Expand(paths []string) ([]*unstructured.Unstructured, error) {
vm, err := spec.jsonnetVM()
if err != nil {
return nil, err
}
defer vm.Destroy()
res := []*unstructured.Unstructured{}
for _, path := range paths {
objs, err := utils.Read(vm, path)
if err != nil {
return nil, fmt.Errorf("Error reading %s: %v", path, err)
}
res = append(res, utils.FlattenToV1(objs)...)
}
return res, nil
}
// JsonnetVM constructs a new jsonnet.VM, according to command line
// flags
func (spec *Expander) jsonnetVM() (*jsonnet.VM, error) {
vm := jsonnet.Make()
for _, p := range spec.EnvJPath {
log.Debugln("Adding jsonnet search path", p)
vm.JpathAdd(p)
}
for _, p := range spec.FlagJpath {
log.Debugln("Adding jsonnet search path", p)
vm.JpathAdd(p)
}
for _, extvar := range spec.ExtVars {
kv := strings.SplitN(extvar, "=", 2)
switch len(kv) {
case 1:
v, present := os.LookupEnv(kv[0])
if present {
vm.ExtVar(kv[0], v)
} else {
return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
}
case 2:
vm.ExtVar(kv[0], kv[1])
}
}
for _, extvar := range spec.ExtVarFiles {
kv := strings.SplitN(extvar, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("Failed to parse ext var files: missing '=' in %s", extvar)
}
v, err := ioutil.ReadFile(kv[1])
if err != nil {
return nil, err
}
vm.ExtVar(kv[0], string(v))
}
for _, tlavar := range spec.TlaVars {
kv := strings.SplitN(tlavar, "=", 2)
switch len(kv) {
case 1:
v, present := os.LookupEnv(kv[0])
if present {
vm.TlaVar(kv[0], v)
} else {
return nil, fmt.Errorf("Missing environment variable: %s", kv[0])
}
case 2:
vm.TlaVar(kv[0], kv[1])
}
}
for _, tlavar := range spec.TlaVarFiles {
kv := strings.SplitN(tlavar, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("Failed to parse tla var files: missing '=' in %s", tlavar)
}
v, err := ioutil.ReadFile(kv[1])
if err != nil {
return nil, err
}
vm.TlaVar(kv[0], string(v))
}
resolver, err := spec.buildResolver()
if err != nil {
return nil, err
}
utils.RegisterNativeFuncs(vm, resolver)
return vm, nil
}
package template
import (
"fmt"
"net/http"
"github.com/ksonnet/kubecfg/utils"
log "github.com/sirupsen/logrus"
)
func (spec *Expander) buildResolver() (utils.Resolver, error) {
ret := resolverErrorWrapper{}
switch spec.FailAction {
case "ignore":
ret.OnErr = func(error) error { return nil }
case "warn":
ret.OnErr = func(err error) error {
log.Warning(err.Error())
return nil
}
case "error":
ret.OnErr = func(err error) error { return err }
default:
return nil, fmt.Errorf("Unknown resolve failure type: %s", spec.FailAction)
}
switch spec.Resolver {
case "noop":
ret.Inner = utils.NewIdentityResolver()
case "registry":
ret.Inner = utils.NewRegistryResolver(&http.Client{
Transport: utils.NewAuthTransport(http.DefaultTransport),
})
default:
return nil, fmt.Errorf("Unknown resolver type: %s", spec.Resolver)
}
return &ret, nil
}
type resolverErrorWrapper struct {
Inner utils.Resolver
OnErr func(error) error
}
func (r *resolverErrorWrapper) Resolve(image *utils.ImageName) error {
err := r.Inner.Resolve(image)
if err != nil {
err = r.OnErr(err)
}
return err
}
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