diff --git a/.travis.yml b/.travis.yml index 95349f58110c4bdeb3bcb76d05722da70bf3279a..43283d79edb76ee743bef0d20b4aa248421adc18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: go go: - 1.8.x - - 1.7.x os: - linux diff --git a/Makefile b/Makefile index 8612b0fa7b5c34f9937b6700ff4e3b8b0f33675f..e9d46b037bacc2def2bb760a69cab2a0925f59f1 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/cmd/delete.go b/cmd/delete.go index 487d9b3d0ce4ff91d2453d23a838785347a0ddcb..4f31ef3a3852695ff815f395af62666a538d8a25 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -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 } diff --git a/cmd/diff.go b/cmd/diff.go index e1cf8dc6b978b64767020a1c97c79868f4e53e68..eab76ed727eb57bce9bef6b22858b88c28019588 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -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{} { diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000000000000000000000000000000000000..ad4a7358801fb28ac28bd221d677788277321059 --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,102 @@ +// 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`, +} diff --git a/cmd/root.go b/cmd/root.go index 9d188b6fef8ca987ac4d47bb6cc187f9dd115eac..b0f9f5c5bcc2acca4f0a7715a8c041ecf5c821fa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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) +} diff --git a/cmd/show.go b/cmd/show.go index 6e98a35e67135b2489afc8e200e6aca3155a8c70..e9c021a6c19a0da50c8655a5ae09456ec86c3055 100644 --- a/cmd/show.go +++ b/cmd/show.go @@ -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 } diff --git a/cmd/show_test.go b/cmd/show_test.go index c404abe810b3429858133b04a343fdc4ef25ffc4..316dc6d000372a7397024fdb58560ab5466feb22 100644 --- a/cmd/show_test.go +++ b/cmd/show_test.go @@ -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"), diff --git a/cmd/update.go b/cmd/update.go index c48696fe45bf2c3e7253d86c89dbc8500a7b855e..810f2b8af91984c8aa8d973eaebad9a5eb861dab 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -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`, } diff --git a/cmd/validate.go b/cmd/validate.go index f44e9b4c0bf003dcd6070a21c6619e6da48c987b..abdeb39640e8b56053e58046ff76cb03fadb7c26 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -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`, } 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..bc58d16ba114ffbfb78da45f94ba63046a38a5ed 100644 --- a/metadata/interface.go +++ b/metadata/interface.go @@ -1,23 +1,25 @@ 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() } diff --git a/metadata/manager.go b/metadata/manager.go index 9898bfed3b3ffbcfec433cbc953632b745270570..2bfb04c78525d726e38df915eb27a0e391b0a903 100644 --- a/metadata/manager.go +++ b/metadata/manager.go @@ -1,11 +1,14 @@ 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) +} diff --git a/metadata/manager_test.go b/metadata/manager_test.go index 1329bc8b9d5385d6f5f9d8227bba8a96c540304f..1e591ac85ce206ee7ad320c63868d20986e56c14 100644 --- a/metadata/manager_test.go +++ b/metadata/manager_test.go @@ -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()) } diff --git a/pkg/kubecfg/common.go b/pkg/kubecfg/common.go new file mode 100644 index 0000000000000000000000000000000000000000..542c3b2625b6692e3e8c68995e0258c16df6a46c --- /dev/null +++ b/pkg/kubecfg/common.go @@ -0,0 +1,34 @@ +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 +} diff --git a/pkg/kubecfg/init.go b/pkg/kubecfg/init.go new file mode 100644 index 0000000000000000000000000000000000000000..454d7dc8c8d1789a3ad0696dbef75f6127463fab --- /dev/null +++ b/pkg/kubecfg/init.go @@ -0,0 +1,25 @@ +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 +} diff --git a/pkg/kubecfg/update.go b/pkg/kubecfg/update.go index 9a95c84e17358a76b7232613b7d08ca04a794c15..dc6916072f4474a3ab5af6ea5295410c2d168d94 100644 --- a/pkg/kubecfg/update.go +++ b/pkg/kubecfg/update.go @@ -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)" diff --git a/template/expander.go b/template/expander.go new file mode 100644 index 0000000000000000000000000000000000000000..d414db8c6ab33db2e46396210ab3c9c13401c381 --- /dev/null +++ b/template/expander.go @@ -0,0 +1,122 @@ +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 +} diff --git a/template/resolver.go b/template/resolver.go new file mode 100644 index 0000000000000000000000000000000000000000..6c18c57474d16764d7e648d57d0912eefa528b8b --- /dev/null +++ b/template/resolver.go @@ -0,0 +1,53 @@ +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 +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/LICENSE b/vendor/github.com/ksonnet/ksonnet-lib/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..8dada3edaf50dbc082c9a125058f25def75e625a --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet/rewrite.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet/rewrite.go new file mode 100644 index 0000000000000000000000000000000000000000..65aa5e964bc8c28a91276ac15088cdb53951cdb7 --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet/rewrite.go @@ -0,0 +1,115 @@ +// Package jsonnet contains a collection of simple rewriting +// facilities that allow us to easily map text from the OpenAPI spec +// to things that are Jsonnet-friendly (e.g., renaming identifiers +// that are Jsonnet keywords, lowerCamelCase'ing names, and so on). +package jsonnet + +import ( + "fmt" + "log" + "strings" + + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec" + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion" +) + +// FieldKey represents the literal text of a key for some JSON object +// field, after rewriting to avoid collisions with Jsonnet keywords. +// For example, for `{foo: ...}`, the `FieldKey` would be `foo`, while +// for `{error: ...}`, the `FieldKey` would be `"error"` (with +// quotation marks, to avoid collisions). +type FieldKey string + +// FuncParam represents the parameter to a Jsonnet function, after +// being rewritten to avoid collisions with Jsonnet keywords and +// normalized to fit the Jsonnet style (i.e., lowerCamelCase) using a +// manual set of custom transformations that change per Kubernetes +// version. For example, in `foo(BarAPI) {...}`, `FuncParam` would be +// `barApi`, and in `foo(error) {...}`, `FuncParam` would be +// `errorParam`. +type FuncParam string + +// Identifier represents any identifier in a Jsonnet program, after +// being normalized to fit the Jsonnet style (i.e., lowerCamelCase) +// using a manual set of custom transformations that change per +// Kubernetes version. For example, `fooAPI` becomes `fooApi`. +type Identifier string + +// RewriteAsFieldKey takes a `PropertyName` and converts it to a valid +// Jsonnet field name. For example, if the `PropertyName` has a value +// of `"error"`, then this would generate an invalid object, `{error: +// ...}`. Hence, this function will quote this string, so that it ends +// up like: `{"error": ...}`. +func RewriteAsFieldKey(text kubespec.PropertyName) FieldKey { + // NOTE: Because the field needs to have precisely the same text as + // the Kubernetes API spec, we do not compute a version-specific ID + // alias as we do for other rewrites. + if _, ok := jsonnetKeywordSet[text]; ok { + return FieldKey(fmt.Sprintf("\"%s\"", text)) + } + return FieldKey(text) +} + +// RewriteAsFuncParam takes a `PropertyName` and converts it to a +// valid Jsonnet function parameter. For example, if the +// `PropertyName` has a value of `"error"`, then this would generate +// an invalid function parameter, `function(error) ...`. Hence, this +// function will alter the identifier, so that it ends up like: +// `function(errorParam) ...`. +// +// NOTE: This transformation involves a hand-curated style change to +// lowerCamelCase (e.g., `fooAPI` -> `fooApi`). This list changes per +// Kubernetes version, according to identifiers that don't conform to +// this style. +func RewriteAsFuncParam( + k8sVersion string, text kubespec.PropertyName, +) FuncParam { + id := RewriteAsIdentifier(k8sVersion, text) + if _, ok := jsonnetKeywordSet[kubespec.PropertyName(id)]; ok { + return FuncParam(fmt.Sprintf("%sParam", id)) + } + return FuncParam(id) +} + +// RewriteAsIdentifier takes a `GroupName`, `ObjectKind`, +// `PropertyName`, or `string`, and converts it to a Jsonnet-style +// Identifier. Typically this includes lower-casing the first letter, +// but also changing initialisms like fooAPI -> fooApi. +// +// NOTE: This transformation involves a hand-curated style change to +// lowerCamelCase (e.g., `fooAPI` -> `fooApi`). This list changes per +// Kubernetes version, according to identifiers that don't conform to +// this style. +func RewriteAsIdentifier( + k8sVersion string, rawID fmt.Stringer, +) Identifier { + var id = rawID.String() + + if len(id) == 0 { + log.Fatalf("Can't lowercase first letter of 0-rune string") + } + kindString := kubeversion.MapIdentifier(k8sVersion, id) + + upper := strings.ToLower(kindString[:1]) + return Identifier(upper + kindString[1:]) +} + +var jsonnetKeywordSet = map[kubespec.PropertyName]string{ + "assert": "assert", + "else": "else", + "error": "error", + "false": "false", + "for": "for", + "function": "function", + "if": "if", + "import": "import", + "importstr": "importstr", + "in": "in", + "local": "local", + "null": "null", + "tailstrict": "tailstrict", + "then": "then", + "self": "self", + "super": "super", + "true": "true", +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/buffer.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/buffer.go new file mode 100644 index 0000000000000000000000000000000000000000..15dc6cced7cee305c3722c5a7da18030fc9f1853 --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/buffer.go @@ -0,0 +1,58 @@ +package ksonnet + +import ( + "bytes" + "fmt" + "strings" +) + +// indentWriter abstracts the task of writing out indented text to a +// buffer. Different components can call `indent` and `dedent` as +// appropriate to specify how indentation needs to change, rather than +// to keep track of the current indentation. +// +// For example, if one component is responsible for writing an array, +// and an element in that array is a function, the component +// responsible for the array need only know to call `indent` after the +// '[' character and `dedent` before the ']' character, while the +// routine responsible for writing out the function can handle its own +// indentation independently. +type indentWriter struct { + depth int + err error + buffer bytes.Buffer +} + +func newIndentWriter() *indentWriter { + var buffer bytes.Buffer + return &indentWriter{ + depth: 0, + err: nil, + buffer: buffer, + } +} + +func (m *indentWriter) writeLine(text string) { + if m.err != nil { + return + } + prefix := strings.Repeat(" ", m.depth) + line := fmt.Sprintf("%s%s\n", prefix, text) + _, m.err = m.buffer.WriteString(line) +} + +func (m *indentWriter) bytes() ([]byte, error) { + if m.err != nil { + return nil, m.err + } + + return m.buffer.Bytes(), nil +} + +func (m *indentWriter) indent() { + m.depth++ +} + +func (m *indentWriter) dedent() { + m.depth-- +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/emit.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/emit.go new file mode 100644 index 0000000000000000000000000000000000000000..908ff43c11742cb8993bfc854d50520b12b2bbe4 --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/emit.go @@ -0,0 +1,888 @@ +package ksonnet + +import ( + "fmt" + "log" + "sort" + "strings" + + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet" + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec" + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion" +) + +// Emit takes a swagger API specification, and returns the text of +// `ksonnet-lib`, written in Jsonnet. +func Emit(spec *kubespec.APISpec, ksonnetLibSHA, k8sSHA *string) ([]byte, error) { + root := newRoot(spec, ksonnetLibSHA, k8sSHA) + + m := newIndentWriter() + root.emit(m) + return m.bytes() +} + +//----------------------------------------------------------------------------- +// Root. +//----------------------------------------------------------------------------- + +// `root` is an abstraction of the root of `k8s.libsonnet`, which can be +// emitted as Jsonnet code using the `emit` method. +// +// `root` contains and manages a set of `groups`, which represent a +// set of Kubernetes API groups (e.g., core, apps, extensions), and +// holds all of the logic required to build the `groups` from an +// `kubespec.APISpec`. +type root struct { + spec *kubespec.APISpec + groups groupSet // set of groups, e.g., core, apps, extensions. + hiddenGroups groupSet + + ksonnetLibSHA *string + k8sSHA *string +} + +func newRoot(spec *kubespec.APISpec, ksonnetLibSHA, k8sSHA *string) *root { + root := root{ + spec: spec, + groups: make(groupSet), + hiddenGroups: make(groupSet), + + ksonnetLibSHA: ksonnetLibSHA, + k8sSHA: k8sSHA, + } + + for defName, def := range spec.Definitions { + root.addDefinition(defName, def) + } + + return &root +} + +func (root *root) emit(m *indentWriter) { + m.writeLine("// AUTOGENERATED from the Kubernetes OpenAPI specification. DO NOT MODIFY.") + m.writeLine(fmt.Sprintf("// Kubernetes version: %s", root.spec.Info.Version)) + + if root.ksonnetLibSHA != nil { + m.writeLine(fmt.Sprintf( + "// SHA of ksonnet-lib HEAD: %s", *root.ksonnetLibSHA)) + } + + if root.ksonnetLibSHA != nil { + m.writeLine(fmt.Sprintf( + "// SHA of Kubernetes HEAD OpenAPI spec is generated from: %s", + *root.k8sSHA)) + } + m.writeLine("") + + m.writeLine("{") + m.indent() + + // Emit in sorted order so that we can diff the output. + for _, group := range root.groups.toSortedSlice() { + group.emit(m) + } + + m.writeLine("local hidden = {") + m.indent() + + for _, hiddenGroup := range root.hiddenGroups.toSortedSlice() { + hiddenGroup.emit(m) + } + + m.dedent() + m.writeLine("},") + + m.dedent() + m.writeLine("}") +} + +func (root *root) addDefinition( + path kubespec.DefinitionName, def *kubespec.SchemaDefinition, +) { + parsedName := path.Parse() + if parsedName.Version == nil { + return + } + apiObject := root.createAPIObject(parsedName, def) + + for propName, prop := range def.Properties { + pm := newPropertyMethod(propName, path, prop, apiObject) + apiObject.properties[propName] = pm + + st := prop.Type + if isMixinRef(pm.ref) || + (st != nil && *st == "array" && prop.Items.Ref != nil) { + typeAliasName := propName + "Type" + ta, ok := apiObject.properties[typeAliasName] + if ok && ta.kind != typeAlias { + log.Panicf( + "Can't create type alias '%s' because a property with that name already exists", typeAliasName) + } + + ta = newPropertyTypeAlias(typeAliasName, path, prop, apiObject) + apiObject.properties[typeAliasName] = ta + } + } +} + +func (root *root) createAPIObject( + parsedName *kubespec.ParsedDefinitionName, def *kubespec.SchemaDefinition, +) *apiObject { + if parsedName.Version == nil { + log.Panicf( + "Can't make API object from name with nil version in path: '%s'", + parsedName.Unparse()) + } + + var groupName kubespec.GroupName + if parsedName.Group == nil { + groupName = "core" + } else { + groupName = *parsedName.Group + } + + var qualifiedName kubespec.GroupName + if len(def.TopLevelSpecs) > 0 && def.TopLevelSpecs[0].Group != "" { + qualifiedName = def.TopLevelSpecs[0].Group + } else { + qualifiedName = groupName + } + + // Separate out top-level definitions from everything else. + var groups groupSet + if len(def.TopLevelSpecs) > 0 { + groups = root.groups + } else { + groups = root.hiddenGroups + } + + group, ok := groups[groupName] + if !ok { + group = newGroup(groupName, qualifiedName, root) + groups[groupName] = group + } + + versionedAPI, ok := group.versionedAPIs[*parsedName.Version] + if !ok { + versionedAPI = newVersionedAPI(*parsedName.Version, group) + group.versionedAPIs[*parsedName.Version] = versionedAPI + } + + apiObject, ok := versionedAPI.apiObjects[parsedName.Kind] + if ok { + log.Panicf("Duplicate object kinds with name '%s'", parsedName.Unparse()) + } + apiObject = newAPIObject(parsedName, versionedAPI, def) + versionedAPI.apiObjects[parsedName.Kind] = apiObject + return apiObject +} + +func (root *root) getAPIObject( + parsedName *kubespec.ParsedDefinitionName, +) *apiObject { + ao, err := root.getAPIObjectHelper(parsedName, false) + if err == nil { + return ao + } + + ao, err = root.getAPIObjectHelper(parsedName, true) + if err != nil { + log.Panic(err.Error()) + } + return ao +} + +func (root *root) getAPIObjectHelper( + parsedName *kubespec.ParsedDefinitionName, hidden bool, +) (*apiObject, error) { + if parsedName.Version == nil { + log.Panicf( + "Can't get API object with nil version: '%s'", parsedName.Unparse()) + } + + var groupName kubespec.GroupName + if parsedName.Group == nil { + groupName = "core" + } else { + groupName = *parsedName.Group + } + + var groups groupSet + if hidden { + groups = root.groups + } else { + groups = root.hiddenGroups + } + + group, ok := groups[groupName] + if !ok { + return nil, fmt.Errorf( + "Could not retrieve object, group in path '%s' doesn't exist", + parsedName.Unparse()) + } + + versionedAPI, ok := group.versionedAPIs[*parsedName.Version] + if !ok { + return nil, fmt.Errorf( + "Could not retrieve object, versioned API in path '%s' doesn't exist", + parsedName.Unparse()) + } + + if apiObject, ok := versionedAPI.apiObjects[parsedName.Kind]; ok { + return apiObject, nil + } + return nil, fmt.Errorf( + "Could not retrieve object, kind in path '%s' doesn't exist", + parsedName.Unparse()) +} + +//----------------------------------------------------------------------------- +// Group. +//----------------------------------------------------------------------------- + +// `group` is an abstract representation of a Kubernetes API group +// (e.g., apps, extensions, core), which can be emitted as Jsonnet +// code using the `emit` method. +// +// `group` contains a set of versioned APIs (e.g., v1, v1beta1, etc.), +// though the logic for creating them is handled largely by `root`. +type group struct { + name kubespec.GroupName // e.g., core, apps, extensions. + qualifiedName kubespec.GroupName // e.g., rbac.authorization.k8s.io. + versionedAPIs versionedAPISet // e.g., v1, v1beta1. + parent *root +} +type groupSet map[kubespec.GroupName]*group +type groupSlice []*group + +func newGroup( + name kubespec.GroupName, qualifiedName kubespec.GroupName, parent *root, +) *group { + return &group{ + name: name, + qualifiedName: qualifiedName, + versionedAPIs: make(versionedAPISet), + parent: parent, + } +} + +func (group *group) root() *root { + return group.parent +} + +func (group *group) emit(m *indentWriter) { + k8sVersion := group.root().spec.Info.Version + mixinName := jsonnet.RewriteAsIdentifier(k8sVersion, group.name) + line := fmt.Sprintf("%s:: {", mixinName) + m.writeLine(line) + m.indent() + + // Emit in sorted order so that we can diff the output. + for _, versioned := range group.versionedAPIs.toSortedSlice() { + versioned.emit(m) + } + + m.dedent() + m.writeLine("},") +} + +func (gs groupSet) toSortedSlice() groupSlice { + groups := groupSlice{} + for _, group := range gs { + groups = append(groups, group) + } + sort.Slice(groups, func(i, j int) bool { + return groups[i].name < groups[j].name + }) + return groups +} + +//----------------------------------------------------------------------------- +// Versioned API. +//----------------------------------------------------------------------------- + +// `versionedAPI` is an abstract representation of a version of a +// Kubernetes API group (e.g., apps.v1beta1, extensions.v1beta1, +// core.v1), which can be emitted as Jsonnet code using the `emit` +// method. +// +// `versionedAPI` contains a set of API objects (e.g., v1.Container, +// v1beta1.Deployment, etc.), though the logic for creating them is +// handled largely by `root`. +type versionedAPI struct { + version kubespec.VersionString // version string, e.g., v1, v1beta1. + apiObjects apiObjectSet // set of objects, e.g, v1.Container. + parent *group +} +type versionedAPISet map[kubespec.VersionString]*versionedAPI +type versionedAPISlice []*versionedAPI + +func newVersionedAPI( + version kubespec.VersionString, parent *group, +) *versionedAPI { + return &versionedAPI{ + version: version, + apiObjects: make(apiObjectSet), + parent: parent, + } +} + +func (va *versionedAPI) root() *root { + return va.parent.parent +} + +func (va *versionedAPI) emit(m *indentWriter) { + // NOTE: Do not need to call `jsonnet.RewriteAsIdentifier`. + line := fmt.Sprintf("%s:: {", va.version) + m.writeLine(line) + m.indent() + + gn := va.parent.qualifiedName + if gn == "core" { + m.writeLine(fmt.Sprintf( + "local apiVersion = {apiVersion: \"%s\"},", va.version)) + } else { + m.writeLine(fmt.Sprintf( + "local apiVersion = {apiVersion: \"%s/%s\"},", gn, va.version)) + } + + // Emit in sorted order so that we can diff the output. + for _, object := range va.apiObjects.toSortedSlice() { + object.emit(m) + } + + m.dedent() + m.writeLine("},") +} + +func (vas versionedAPISet) toSortedSlice() versionedAPISlice { + versionedAPIs := versionedAPISlice{} + for _, va := range vas { + versionedAPIs = append(versionedAPIs, va) + } + sort.Slice(versionedAPIs, func(i, j int) bool { + return versionedAPIs[i].version < versionedAPIs[j].version + }) + return versionedAPIs +} + +//----------------------------------------------------------------------------- +// API object. +//----------------------------------------------------------------------------- + +// `apiObject` is an abstract representation of a Kubernetes API +// object (e.g., v1.Container, v1beta1.Deployment), which can be +// emitted as Jsonnet code using the `emit` method. +// +// `apiObject` contains a set of property methods and mixins which +// formulate the basis of much of ksonnet-lib's programming surface. +// The logic for creating them is handled largely by `root`. +type apiObject struct { + name kubespec.ObjectKind // e.g., `Container` in `v1.Container` + properties propertySet // e.g., container.image, container.env + parsedName *kubespec.ParsedDefinitionName + comments comments + parent *versionedAPI + isTopLevel bool +} +type apiObjectSet map[kubespec.ObjectKind]*apiObject +type apiObjectSlice []*apiObject + +func newAPIObject( + name *kubespec.ParsedDefinitionName, parent *versionedAPI, + def *kubespec.SchemaDefinition, +) *apiObject { + isTopLevel := len(def.TopLevelSpecs) > 0 + comments := newComments(def.Description) + return &apiObject{ + name: name.Kind, + parsedName: name, + properties: make(propertySet), + comments: comments, + parent: parent, + isTopLevel: isTopLevel, + } +} + +func (ao apiObject) toRefPropertyMethod( + name kubespec.PropertyName, path kubespec.DefinitionName, parent *apiObject, +) *property { + return &property{ + ref: path.AsObjectRef(), + schemaType: nil, + itemTypes: kubespec.Items{}, + name: name, + path: path, + comments: ao.comments, + parent: parent, + } +} + +func (ao *apiObject) root() *root { + return ao.parent.parent.parent +} + +func (ao *apiObject) emit(m *indentWriter) { + k8sVersion := ao.root().spec.Info.Version + jsonnetName := kubespec.ObjectKind( + jsonnet.RewriteAsIdentifier(k8sVersion, ao.name)) + if _, ok := ao.parent.apiObjects[jsonnetName]; ok { + log.Panicf( + "Tried to lowercase first character of object kind '%s', but lowercase name was already present in version '%s'", + jsonnetName, + ao.parent.version) + } + + ao.comments.emit(m) + + m.writeLine(fmt.Sprintf("%s:: {", jsonnetName)) + m.indent() + + if ao.isTopLevel { + // NOTE: It is important to NOT capitalize `ao.name` here. + m.writeLine(fmt.Sprintf("local kind = {kind: \"%s\"},", ao.name)) + } + ao.emitConstructors(m) + + for _, pm := range ao.properties.sortAndFilterBlacklisted() { + // Skip special properties and fields that `$ref` another API + // object type, since those will go in the `mixin` namespace. + if isSpecialProperty(pm.name) || isMixinRef(pm.ref) { + continue + } + pm.emit(m) + } + + // Emit the properties that `$ref` another API object type in the + // `mixin:: {` namespace. + m.writeLine("mixin:: {") + m.indent() + + for _, pm := range ao.properties.sortAndFilterBlacklisted() { + // TODO: Emit mixin code also for arrays whose elements are + // `$ref`. + if !isMixinRef(pm.ref) { + continue + } + + pm.emit(m) + } + + m.dedent() + m.writeLine("},") + + m.dedent() + m.writeLine("},") +} + +// `emitAsRefMixins` recursively emits an API object as a collection +// of mixin methods, particularly when another API object has a +// property that uses `$ref` to reference the current API object. +// +// For example, `v1beta1.Deployment` has a field, `spec`, which is of +// type `v1beta1.DeploymentSpec`. In this case, we'd like to +// recursively capture all the properties of `v1beta1.DeploymentSpec` +// and create mixin methods, so that we can do something like +// `someDeployment + deployment.mixin.spec.minReadySeconds(3)`. +func (ao *apiObject) emitAsRefMixins( + m *indentWriter, p *property, parentMixinName *string, +) { + k8sVersion := ao.root().spec.Info.Version + functionName := jsonnet.RewriteAsIdentifier(k8sVersion, p.name) + paramName := jsonnet.RewriteAsFuncParam(k8sVersion, p.name) + fieldName := jsonnet.RewriteAsFieldKey(p.name) + mixinName := fmt.Sprintf("__%sMixin", functionName) + var mixinText string + if parentMixinName == nil { + mixinText = fmt.Sprintf( + "local %s(%s) = {%s+: %s},", mixinName, paramName, fieldName, paramName) + } else { + mixinText = fmt.Sprintf( + "local %s(%s) = %s({%s+: %s}),", + mixinName, paramName, *parentMixinName, fieldName, paramName) + } + + if _, ok := ao.parent.apiObjects[kubespec.ObjectKind(functionName)]; ok { + log.Panicf( + "Tried to lowercase first character of object kind '%s', but lowercase name was already present in version '%s'", + functionName, + ao.parent.version) + } + + // NOTE: Comments are emitted by `property#emit`, before we + // call this method. + + line := fmt.Sprintf("%s:: {", functionName) + m.writeLine(line) + m.indent() + + m.writeLine(mixinText) + m.writeLine( + fmt.Sprintf("mixinInstance(%s):: %s(%s),", paramName, mixinName, paramName)) + + for _, pm := range ao.properties.sortAndFilterBlacklisted() { + if isSpecialProperty(pm.name) { + continue + } + pm.emitAsRefMixin(m, mixinName) + } + + m.dedent() + m.writeLine("},") +} + +func (ao *apiObject) emitConstructors(m *indentWriter) { + k8sVersion := ao.root().spec.Info.Version + path := ao.parsedName.Unparse() + + specs, ok := kubeversion.ConstructorSpec(k8sVersion, path) + if !ok { + ao.emitConstructor(m, constructorName, []kubeversion.CustomConstructorParam{}) + return + } + + for _, spec := range specs { + ao.emitConstructor(m, spec.ID, spec.Params) + } +} + +func (ao *apiObject) emitConstructor( + m *indentWriter, id string, params []kubeversion.CustomConstructorParam, +) { + // Panic if a function with the constructor's name already exists. + specName := kubespec.PropertyName(id) + if dm, ok := ao.properties[specName]; ok { + log.Panicf( + "Attempted to create constructor, but '%s' property already existed at '%s'", + specName, dm.path) + } + + // Default body of the constructor. Usually either `apiVersion + + // kind` or `{}`. + var defaultSetters []string + if ao.isTopLevel { + defaultSetters = specialPropertiesList + } else { + defaultSetters = []string{"{}"} + } + + // Build parameters and body of constructor. Considering the example + // of the constructor of `v1.Container`: + // + // new(name, image):: self.name(name) + self.image(image), + // + // Here we want to (1) assemble the parameter list (i.e., `name` and + // `image`), as well as the body (i.e., the calls to `self.name` and + // so on). + paramLiterals := []string{} + setters := defaultSetters + for _, param := range params { + // Add the param to the param list, including default value if + // applicable. + if param.DefaultValue != nil { + paramLiterals = append( + paramLiterals, fmt.Sprintf("%s=%s", param.ID, *param.DefaultValue)) + } else { + paramLiterals = append(paramLiterals, param.ID) + } + + // Add an element to the body (e.g., `self.name` above). + if param.RelativePath == nil { + prop, ok := ao.properties[kubespec.PropertyName(param.ID)] + if !ok { + log.Panicf( + "Attempted to create constructor, but property '%s' does not exist", + param.ID) + } + setters = append( + setters, fmt.Sprintf("self.%s(%s)", prop.name, param.ID)) + } else { + // TODO(hausdorff): We may want to verify this relative path + // exists. + setters = append( + setters, fmt.Sprintf("self.%s(%s)", *param.RelativePath, param.ID)) + } + } + + // Write out constructor. + paramsText := strings.Join(paramLiterals, ", ") + bodyText := strings.Join(setters, " + ") + m.writeLine(fmt.Sprintf("%s(%s):: %s,", specName, paramsText, bodyText)) +} + +func (aos apiObjectSet) toSortedSlice() apiObjectSlice { + apiObjects := apiObjectSlice{} + for _, apiObject := range aos { + apiObjects = append(apiObjects, apiObject) + } + sort.Slice(apiObjects, func(i, j int) bool { + return apiObjects[i].name < apiObjects[j].name + }) + return apiObjects +} + +//----------------------------------------------------------------------------- +// Property method. +//----------------------------------------------------------------------------- + +type propertyKind int + +const ( + method propertyKind = iota + typeAlias +) + +// `property` is an abstract representation of a ksonnet-lib's +// property methods, which can be emitted as Jsonnet code using the +// `emit` method. +// +// For example, ksonnet-lib exposes many functions such as +// `v1.container.image`, which can be added together with the `+` +// operator to construct a complete image. `property` is an +// abstract representation of these so-called "property methods". +// +// `property` contains the name of the property given in the +// `apiObject` that is its parent (for example, `Deployment` has a +// field called `containers`, which is an array of `v1.Container`), as +// well as the `kubespec.PropertyName`, which contains information +// required to generate the Jsonnet code. +// +// The logic for creating them is handled largely by `root`. +type property struct { + kind propertyKind + ref *kubespec.ObjectRef + schemaType *kubespec.SchemaType + itemTypes kubespec.Items + name kubespec.PropertyName // e.g., image in container.image. + path kubespec.DefinitionName + comments comments + parent *apiObject +} +type propertySet map[kubespec.PropertyName]*property +type propertySlice []*property + +func newPropertyMethod( + name kubespec.PropertyName, path kubespec.DefinitionName, + prop *kubespec.Property, parent *apiObject, +) *property { + comments := newComments(prop.Description) + return &property{ + kind: method, + ref: prop.Ref, + schemaType: prop.Type, + itemTypes: prop.Items, + name: name, + path: path, + comments: comments, + parent: parent, + } +} + +func newPropertyTypeAlias( + name kubespec.PropertyName, path kubespec.DefinitionName, + prop *kubespec.Property, parent *apiObject, +) *property { + comments := newComments(prop.Description) + return &property{ + kind: typeAlias, + ref: prop.Ref, + schemaType: prop.Type, + itemTypes: prop.Items, + name: name, + path: path, + comments: comments, + parent: parent, + } +} + +func (p *property) root() *root { + return p.parent.parent.parent.parent +} + +func (p *property) emit(m *indentWriter) { + p.emitHelper(m, nil) +} + +// `emitAsRefMixin` will emit a property as a mixin method, so that it +// can be "mixed in" to alter an existing object. +// +// For example if we have a fully-formed deployment object, +// `someDeployment`, we'd like to be able to do something like +// `someDeployment + deployment.mixin.spec.minReadySeconds(3)` to "mix +// in" a change to the `spec.minReadySeconds` field. +// +// This method will take the `property`, which specifies a +// property method, and use it to emit such a "mixin method". +func (p *property) emitAsRefMixin( + m *indentWriter, parentMixinName string, +) { + p.emitHelper(m, &parentMixinName) +} + +func (p *property) emitAsTypeAlias(m *indentWriter) { + var path kubespec.DefinitionName + if p.ref != nil { + path = *p.ref.Name() + } else { + path = *p.itemTypes.Ref.Name() + } + parsedPath := path.Parse() + if parsedPath.Version == nil { + log.Printf("Could not emit type alias for '%s'\n", path) + return + } + + // Chop the `Type` off the end of the type alias name, rewrite the + // "base" of the type alias, and then append `Type` to the end + // again. + // + // Why: the desired behavior is for a rewrite rule to apply to both + // a method and its type alias. For example, if we specify that + // `scaleIO` should be rewritten `scaleIo`, then we'd like the type + // alias to be emitted as `scaleIoType`, not `scaleIOType`, + // automatically, so that the user doesn't have to specify another, + // separate rule for the type alias itself. + k8sVersion := p.root().spec.Info.Version + trimmedName := kubespec.PropertyName(strings.TrimSuffix(string(p.name), "Type")) + typeName := jsonnet.RewriteAsIdentifier(k8sVersion, trimmedName) + "Type" + + var group kubespec.GroupName + if parsedPath.Group == nil { + group = "core" + } else { + group = *parsedPath.Group + } + + id := jsonnet.RewriteAsIdentifier(k8sVersion, parsedPath.Kind) + line := fmt.Sprintf( + "%s:: hidden.%s.%s.%s,", + typeName, group, parsedPath.Version, id) + + m.writeLine(line) +} + +// `emitHelper` emits the Jsonnet program text for a `property`, +// handling both the case that it's a mixin (i.e., `parentMixinName != +// nil`), and the case that it's a "normal", non-mixin property method +// (i.e., `parentMixinName == nil`). +// +// NOTE: To get `emitHelper` to emit this property as a mixin, it is +// REQUIRED for `parentMixinName` to be non-nil; likewise, to get +// `emitHelper` to emit this property as a normal, non-mixin property +// method, it is necessary for `parentMixinName == nil`. +func (p *property) emitHelper( + m *indentWriter, parentMixinName *string, +) { + if p.kind == typeAlias { + p.emitAsTypeAlias(m) + return + } + + p.comments.emit(m) + + k8sVersion := p.root().spec.Info.Version + functionName := jsonnet.RewriteAsIdentifier(k8sVersion, p.name) + paramName := jsonnet.RewriteAsFuncParam(k8sVersion, p.name) + fieldName := jsonnet.RewriteAsFieldKey(p.name) + signature := fmt.Sprintf("%s(%s)::", functionName, paramName) + + if isMixinRef(p.ref) { + parsedRefPath := p.ref.Name().Parse() + apiObject := p.root().getAPIObject(parsedRefPath) + apiObject.emitAsRefMixins(m, p, parentMixinName) + } else if p.ref != nil && !isMixinRef(p.ref) { + var body string + if parentMixinName == nil { + body = fmt.Sprintf("{%s: %s}", fieldName, paramName) + } else { + body = fmt.Sprintf("%s({%s: %s})", *parentMixinName, fieldName, paramName) + } + line := fmt.Sprintf("%s %s,", signature, body) + m.writeLine(line) + } else if p.schemaType != nil { + paramType := *p.schemaType + + var body string + switch paramType { + case "array": + if parentMixinName == nil { + body = fmt.Sprintf( + "if std.type(%s) == \"array\" then {%s+: %s} else {%s+: [%s]}", + paramName, fieldName, paramName, fieldName, paramName, + ) + } else { + body = fmt.Sprintf( + "if std.type(%s) == \"array\" then %s({%s+: %s}) else %s({%s+: [%s]})", + paramName, *parentMixinName, fieldName, paramName, *parentMixinName, + fieldName, paramName, + ) + } + case "integer", "string", "boolean": + if parentMixinName == nil { + body = fmt.Sprintf("{%s: %s}", fieldName, paramName) + } else { + body = fmt.Sprintf("%s({%s: %s})", *parentMixinName, fieldName, paramName) + } + case "object": + if parentMixinName == nil { + body = fmt.Sprintf("{%s+: %s}", fieldName, paramName) + } else { + body = fmt.Sprintf("%s({%s+: %s})", *parentMixinName, fieldName, paramName) + } + default: + log.Panicf("Unrecognized type '%s'", paramType) + } + + line := fmt.Sprintf("%s %s,", signature, body) + m.writeLine(line) + } else { + log.Panicf("Neither a type nor a ref") + } +} + +func (aos propertySet) sortAndFilterBlacklisted() propertySlice { + properties := propertySlice{} + for _, pm := range aos { + k8sVersion := pm.root().spec.Info.Version + var name kubespec.PropertyName + if pm.kind == typeAlias { + name = kubespec.PropertyName(strings.TrimSuffix(string(pm.name), "Type")) + } else { + name = pm.name + } + if kubeversion.IsBlacklistedProperty(k8sVersion, pm.path, name) { + continue + } else if pm.ref != nil { + if parsed := pm.ref.Name().Parse(); parsed.Version == nil { + // TODO: Might want to error out here. + continue + } + } + properties = append(properties, pm) + } + sort.Slice(properties, func(i, j int) bool { + return properties[i].name < properties[j].name + }) + return properties +} + +//----------------------------------------------------------------------------- +// Comments. +//----------------------------------------------------------------------------- + +type comments []string + +func newComments(text string) comments { + return strings.Split(text, "\n") +} + +func (cs *comments) emit(m *indentWriter) { + for _, comment := range *cs { + if comment == "" { + // Don't create trailing space if comment is empty. + m.writeLine("//") + } else { + m.writeLine(fmt.Sprintf("// %s", comment)) + } + } +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/util.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/util.go new file mode 100644 index 0000000000000000000000000000000000000000..b02a9bd4d31add0eb782990f1807599b28fde6b8 --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet/util.go @@ -0,0 +1,29 @@ +package ksonnet + +import ( + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec" +) + +const constructorName = "new" + +// isMixinRef will check whether a `ObjectRef` refers to an API object +// that can be turned into a mixin. This should be true of the vast +// majority of non-nil `ObjectRef`s. The most common exception is +// `IntOrString`, which should not be turned into a mixin, and should +// instead by transformed into a property method that behaves +// identically to one taking an int or a ref as argument. +func isMixinRef(or *kubespec.ObjectRef) bool { + return or != nil && *or != "#/definitions/io.k8s.apimachinery.pkg.util.intstr.IntOrString" +} + +var specialProperties = map[kubespec.PropertyName]kubespec.PropertyName{ + "apiVersion": "apiVersion", + "kind": "kind", +} + +var specialPropertiesList = []string{"apiVersion", "kind"} + +func isSpecialProperty(pn kubespec.PropertyName) bool { + _, ok := specialProperties[pn] + return ok +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/parsing.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/parsing.go new file mode 100644 index 0000000000000000000000000000000000000000..d154fd8b0d7f3f2919256e290682baa186507296 --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/parsing.go @@ -0,0 +1,235 @@ +package kubespec + +import ( + "fmt" + "log" + "strings" +) + +//----------------------------------------------------------------------------- +// Utility methods for `DefinitionName` and `ObjectRef`. +//----------------------------------------------------------------------------- + +// Parse will parse a `DefinitionName` into a structured +// `ParsedDefinitionName`. +func (dn *DefinitionName) Parse() *ParsedDefinitionName { + split := strings.Split(string(*dn), ".") + if len(split) < 6 { + log.Fatalf("Failed to parse definition name '%s'", string(*dn)) + } else if split[0] != "io" || split[1] != "k8s" || split[3] != "pkg" { + log.Fatalf("Failed to parse definition name '%s'", string(*dn)) + } + + codebase := split[2] + + if split[4] == "api" { + // Name is something like: `io.k8s.kubernetes.pkg.api.v1.LimitRangeSpec`. + if len(split) < 7 { + log.Fatalf( + "Expected >= 7 path components for package 'api' in path: '%s'", + string(*dn)) + } + versionString := VersionString(split[5]) + return &ParsedDefinitionName{ + PackageType: Core, + Codebase: codebase, + Group: nil, + Version: &versionString, + Kind: ObjectKind(split[6]), + } + } else if split[4] == "apis" { + // Name is something like: `io.k8s.kubernetes.pkg.apis.batch.v1.JobList`. + if len(split) < 8 { + log.Fatalf( + "Expected >= 8 path components for package 'apis' in path: '%s'", + string(*dn)) + } + groupName := GroupName(split[5]) + versionString := VersionString(split[6]) + return &ParsedDefinitionName{ + PackageType: APIs, + Codebase: codebase, + Group: &groupName, + Version: &versionString, + Kind: ObjectKind(split[7]), + } + } else if split[4] == "util" { + if len(split) < 7 { + log.Fatalf( + "Expected >= 7 path components for package 'api' in path: '%s'", + string(*dn)) + } + versionString := VersionString(split[5]) + return &ParsedDefinitionName{ + PackageType: Util, + Codebase: codebase, + Group: nil, + Version: &versionString, + Kind: ObjectKind(split[6]), + } + } else if split[4] == "runtime" { + // Name is something like: `io.k8s.apimachinery.pkg.runtime.RawExtension`. + return &ParsedDefinitionName{ + PackageType: Runtime, + Codebase: codebase, + Group: nil, + Version: nil, + Kind: ObjectKind(split[5]), + } + } else if split[4] == "version" { + // Name is something like: `io.k8s.apimachinery.pkg.version.Info`. + return &ParsedDefinitionName{ + PackageType: Version, + Codebase: codebase, + Group: nil, + Version: nil, + Kind: ObjectKind(split[5]), + } + } + + log.Fatalf("Unknown package name '%s' in path: '%s'", split[4], string(*dn)) + return nil +} + +// Name parses a `DefinitionName` from an `ObjectRef`. `ObjectRef`s +// that refer to a definition contain two parts: (1) a special prefix, +// and (2) a `DefinitionName`, so this function simply strips the +// prefix off. +func (or *ObjectRef) Name() *DefinitionName { + defn := "#/definitions/" + ref := string(*or) + if !strings.HasPrefix(ref, defn) { + log.Fatalln(ref) + } + name := DefinitionName(strings.TrimPrefix(ref, defn)) + return &name +} + +func (dn DefinitionName) AsObjectRef() *ObjectRef { + or := ObjectRef("#/definitions/" + dn) + return &or +} + +//----------------------------------------------------------------------------- +// Parsed definition name. +//----------------------------------------------------------------------------- + +// Package represents the type of the definition, either `APIs`, which +// have API groups (e.g., extensions, apps, meta, and so on), or +// `Core`, which does not. +type Package int + +const ( + // Core is a package that contains the Kubernetes Core objects. + Core Package = iota + + // APIs is a set of non-core packages grouped loosely by semantic + // functionality (e.g., apps, extensions, and so on). + APIs + + // + // Internal packages. + // + + // Util is a package that contains utilities used for both testing + // and running Kubernetes. + Util + + // Runtime is a package that contains various utilities used in the + // Kubernetes runtime. + Runtime + + // Version is a package that supplies version information collected + // at build time. + Version +) + +// ParsedDefinitionName is a parsed version of a fully-qualified +// OpenAPI spec name. For example, +// `io.k8s.kubernetes.pkg.api.v1.Container` would parse into an +// instance of the struct below. +type ParsedDefinitionName struct { + PackageType Package + Codebase string + Group *GroupName // Pointer because it's optional. + Version *VersionString // Pointer because it's optional. + Kind ObjectKind +} + +// GroupName represetents a Kubernetes group name (e.g., apps, +// extensions, etc.) +type GroupName string + +func (gn GroupName) String() string { + return string(gn) +} + +// ObjectKind represents the `kind` of a Kubernetes API object (e.g., +// Service, Deployment, etc.) +type ObjectKind string + +func (ok ObjectKind) String() string { + return string(ok) +} + +// VersionString is the string representation of an API version (e.g., +// v1, v1beta1, etc.) +type VersionString string + +func (vs VersionString) String() string { + return string(vs) +} + +// Unparse transforms a `ParsedDefinitionName` back into its +// corresponding string, e.g., +// `io.k8s.kubernetes.pkg.api.v1.Container`. +func (p *ParsedDefinitionName) Unparse() DefinitionName { + switch p.PackageType { + case Core: + { + return DefinitionName(fmt.Sprintf( + "io.k8s.%s.pkg.api.%s.%s", + p.Codebase, + *p.Version, + p.Kind)) + } + case Util: + { + return DefinitionName(fmt.Sprintf( + "io.k8s.%s.pkg.util.%s.%s", + p.Codebase, + *p.Version, + p.Kind)) + } + case APIs: + { + return DefinitionName(fmt.Sprintf( + "io.k8s.%s.pkg.apis.%s.%s.%s", + p.Codebase, + *p.Group, + *p.Version, + p.Kind)) + } + case Version: + { + return DefinitionName(fmt.Sprintf( + "io.k8s.%s.pkg.version.%s", + p.Codebase, + p.Kind)) + } + case Runtime: + { + return DefinitionName(fmt.Sprintf( + "io.k8s.%s.pkg.runtime.%s", + p.Codebase, + p.Kind)) + } + default: + { + log.Fatalf( + "Failed to unparse definition name, did not recognize kind '%d'", + p.PackageType) + return "" + } + } +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/swagger.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/swagger.go new file mode 100644 index 0000000000000000000000000000000000000000..2310d1a9028260e355703e9f53667396f1472c5e --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec/swagger.go @@ -0,0 +1,108 @@ +package kubespec + +// APISpec represents an OpenAPI specification of an API. +type APISpec struct { + SwaggerVersion string `json:"swagger"` + Info *SchemaInfo `json:"info"` + Definitions SchemaDefinitions `json:"definitions"` + + // Fields we currently ignore: + // - paths + // - securityDefinitions + // - security + + // Not part of the OpenAPI spec. Filled in later. + FilePath string + Text []byte +} + +// SchemaInfo contains information about the the API represented with +// `APISpec`. For example, `title` might be `"Kubernetes"`, and +// `version` might be `"v1.7.0"`. +type SchemaInfo struct { + Title string `json:"title"` + Version string `json:"version"` +} + +// SchemaDefinition is an API object definition. For example, this +// might contain a name (e.g., `v1.APIGroup`), a set of properties +// (e.g., `apiVersion`, `kind`, and so on), and the names of required +// properties. +type SchemaDefinition struct { + Type *SchemaType `json:"type"` + Description string `json:"description"` // nullable. + Required []string `json:"required"` // nullable. + Properties Properties `json:"properties"` // nullable. + TopLevelSpecs TopLevelSpecs `json:"x-kubernetes-group-version-kind"` +} + +// TopLevelSpec is a property that exists on `SchemaDefinition`s for +// top-level API objects. +type TopLevelSpec struct { + Group GroupName `json:"Group"` + Version VersionString `json:"Version"` + Kind ObjectKind `json:"Kind"` +} +type TopLevelSpecs []*TopLevelSpec + +// SchemaDefinitions is a named collection of `SchemaDefinition`s, +// represented as a collection mapping definition name -> +// `SchemaDefinition`. +type SchemaDefinitions map[DefinitionName]*SchemaDefinition + +// Property represents an object property for some API object. For +// example, `v1.APIGroup` might contain a property called +// `apiVersion`, which would be specifid by a `Property`. +type Property struct { + Description string `json:"description"` + Type *SchemaType `json:"type"` + Ref *ObjectRef `json:"$ref"` + Items Items `json:"items"` // nil unless Type == "array". +} + +// Properties is a named collection of `Properties`s, represented as a +// collection mapping definition name -> `Properties`. +type Properties map[PropertyName]*Property + +// Items represents the type of an element in an array. Usually this +// is used to fully specify a `Property` object whose `type` field is +// `"array"`. +type Items struct { + Ref *ObjectRef `json:"$ref"` + + // Ignored fields: + // - Type *SchemaType `json:"type"` + // - Format *string `json:"format"` +} + +// SchemaType represents the type of some object in an API spec. For +// example, a property might have type `string`. +type SchemaType string + +func (st SchemaType) String() string { + return string(st) +} + +// ObjectRef represents a reference to some API object. For example, +// `#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta` +type ObjectRef string + +func (or ObjectRef) String() string { + return string(or) +} + +// PropertyName represents the name of a property. For example, +// `apiVersion` or `kind`. +type PropertyName string + +func (pn PropertyName) String() string { + return string(pn) +} + +// DefinitionName represents the name of a definition. For example, +// `v1.APIGroup`. +type DefinitionName string + +func (dn DefinitionName) String() string { + return string(dn) +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/blacklist.jq b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/blacklist.jq new file mode 100755 index 0000000000000000000000000000000000000000..99bcca7e25eed0a0340f098d3c6a9aaafdde5ec5 --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/blacklist.jq @@ -0,0 +1,103 @@ +#!/usr/bin/env jq -S -f + + +# ----------------------------------------------------------------------------- +# USAGE NOTES. +# +# This `jq` script will generate a list of top-level Kubernetes API +# objects that contain either (or both of): +# +# 1. a property with the name `"status"`, or +# 2. a property whose type is `meta.v1.ListMeta`. +# +# For example: +# +# { +# "io.k8s.apimachinery.pkg.apis.meta.v1.Status": [ +# "status", "metadata" +# ] +# } +# +# This would indicate that the fields `metadata` and `status` are to +# be blacklisted in the object `meta.v1.Status`. +# +# +# Usage: +# cat swagger.json | jq -S -f blacklist.jq +# +# Or, if you are on an OS with jq > v1.4 +# cat swagger.json | ./blacklist.jq +# +# NOTE: It is very important to pass the -S flag here, because sorting +# the object keys makes the output diffable. +# ----------------------------------------------------------------------------- + + +# has_status_prop takes an Kubernetes API object definition from the +# swagger spec, and outputs a boolean indicating whether that API +# object has a property called `status`. +# +# For example, the input might be a +# `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment` object, which +# does indeed have a `status` field. +def has_status_prop: + . as $definition + | if $definition.properties.status != null then true else false end; + +# property_has_listmeta_type takes the property of a Kubernetes API +# object definition, and returns a bool indicating whether its type is +# a `$ref` of `meta.v1.ListMeta`. +# +# For example, `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment` +# does not have a property with a type that is a `$ref` to +# `meta.v1.ListMeta`. +def property_has_listmeta_type: + . as $property + | $property["$ref"] != null and + $property["$ref"] == "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"; + +# props_with_listmeta_type returns the names of all properties in some +# Kubernetes API object definition whose type is `meta.v1.ListMeta`. +# +# For example, `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment` +# does not contain any properties with this type, so we would return +# an empty array, while another object might return a list of names. +def props_with_listmeta_type: [ + . as $definition + | select($definition.properties != null) + | $definition.properties + | to_entries[] + | select(.value | property_has_listmeta_type) + | .key +]; + +# entry_blacklist_props takes a key/value pair representing a +# Kubernetes API object and its name, and returns a list of properties +# that are blacklisted. +# +# For example, `.key` might be +# `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment`, while `.value` +# woudl be the actual swagger specification of the `Deployment` +# object. +def entry_blacklist_props: + .value as $definition + | ($definition | has_status_prop) as $has_status_prop + | ($definition | props_with_listmeta_type) as $props_with_listmeta_type + | ($props_with_listmeta_type | length > 0) as $has_listmeta_type_props + | if $has_status_prop and $has_listmeta_type_props + then {(.key): (["status"] | .+ $props_with_listmeta_type)} + elif $has_status_prop + then {(.key): ["status"]} + elif $has_listmeta_type_props + then {(.key): $props_with_listmeta_type} + else {(.key): []} + end; + +def create_blacklist: + [ .definitions | to_entries[] | entry_blacklist_props ] + | add + | with_entries(select(.value | length > 0)); + + +# Execute. +create_blacklist diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/data.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/data.go new file mode 100644 index 0000000000000000000000000000000000000000..a455bebc3a833c880eb07f14e8b10d9a4bc29b2e --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/data.go @@ -0,0 +1,254 @@ +package kubeversion + +//----------------------------------------------------------------------------- +// Kubernetes version-specific data for customizing code that's +// emitted. +//----------------------------------------------------------------------------- + +var versions = map[string]versionData{ + "v1.7.0": versionData{ + idAliases: map[string]string{ + // Properties of objects. Stuff like `cinder.volumeId`. + "hostIPC": "hostIpc", + "hostPID": "hostPid", + "targetCPUUtilizationPercentage": "targetCpuUtilizationPercentage", + "externalID": "externalId", + "podCIDR": "podCidr", + "providerID": "providerId", + "bootID": "bootId", + "machineID": "machineId", + "systemUUID": "systemUuid", + "volumeID": "volumeId", + "diskURI": "diskUri", + "targetWWNs": "targetWwns", + "datasetUUID": "datasetUuid", + "pdID": "pdId", + "scaleIO": "scaleIo", + "podIP": "podIp", + "hostIP": "hostIp", + "clusterIP": "clusterIp", + "externalIPs": "externalIps", + "loadBalancerIP": "loadBalancerIp", + "containerID": "containerId", + "imageID": "imageId", + "serverAddressByClientCIDRs": "serverAddressByClientCidrs", + "clientCIDR": "clientCidr", + "nonResourceURLs": "nonResourceUrls", + "currentCPUUtilizationPercentage": "currentCpuUtilizationPercentage", + "downwardAPI": "downwardApi", + + // Types. These have capitalized first letters, and exist in + // places like `core.v1.AWSElasticBlockStoreVolumeSource`. + "AWSElasticBlockStoreVolumeSource": "awsElasticBlockStoreVolumeSource", + "CephFSVolumeSource": "cephFsVolumeSource", + "DownwardAPIProjection": "downwardApiProjection", + "DownwardAPIVolumeFile": "downwardApiVolumeFile", + "DownwardAPIVolumeSource": "downwardApiVolumeSource", + "FCVolumeSource": "fcVolumeSource", + "GCEPersistentDiskVolumeSource": "gcePersistentDiskVolumeSource", + "HTTPGetAction": "httpGetAction", + "HTTPHeader": "httpHeader", + "ISCSIVolumeSource": "iscsiVolumeSource", + "NFSVolumeSource": "nfsVolumeSource", + "RBDVolumeSource": "rbdVolumeSource", + "SELinuxOptions": "seLinuxOptions", + "ScaleIOVolumeSource": "scaleIoVolumeSource", + "TCPSocketAction": "tcpSocketAction", + "APIVersion": "apiVersion", + "FSGroupStrategyOptions": "fsGroupStrategyOptions", + "HTTPIngressPath": "httpIngressPath", + "HTTPIngressRuleValue": "httpIngressRuleValue", + "IDRange": "idRange", + "IngressTLS": "ingressTls", + "SELinuxStrategyOptions": "seLinuxStrategyOptions", + "APIGroup": "apiGroup", + "APIGroupList": "apiGroupList", + "APIResource": "apiResource", + "APIResourceList": "apiResourceList", + "APIVersions": "apiVersions", + "ServerAddressByClientCIDR": "serverAddressByClientCidr", + }, + constructorSpecs: map[string][]CustomConstructorSpec{ + "io.k8s.kubernetes.pkg.api.v1.Container": []CustomConstructorSpec{ + newConstructor("new", newParam("name"), newParam("image")), + }, + "io.k8s.kubernetes.pkg.api.v1.ContainerPort": []CustomConstructorSpec{ + newConstructor("new", newParam("containerPort")), + newConstructor("newNamed", newParam("name"), newParam("containerPort")), + }, + "io.k8s.kubernetes.pkg.api.v1.EnvVar": []CustomConstructorSpec{ + newConstructor("new", newParam("name"), newParam("value")), + newConstructor( + "fromSecretRef", + newParam("name"), + newParamNestedRef("secretRefName", "mixin.valueFrom.secretKeyRef.name"), + newParamNestedRef("secretRefKey", "mixin.valueFrom.secretKeyRef.key")), + newConstructor( + "fromFieldPath", + newParam("name"), + newParamNestedRef("fieldPath", "mixin.valueFrom.fieldRef.fieldPath")), + }, + "io.k8s.kubernetes.pkg.api.v1.KeyToPath": []CustomConstructorSpec{ + newConstructor("new", newParam("key"), newParam("path")), + }, + "io.k8s.kubernetes.pkg.api.v1.Service": []CustomConstructorSpec{ + newConstructor( + "new", + newParamNestedRef("name", "mixin.metadata.name"), + newParamNestedRef("selector", "mixin.spec.selector"), + newParamNestedRef("ports", "mixin.spec.ports")), + }, + "io.k8s.kubernetes.pkg.api.v1.ServicePort": []CustomConstructorSpec{ + newConstructor("new", newParam("port"), newParam("targetPort")), + newConstructor("newNamed", newParam("name"), newParam("port"), newParam("targetPort")), + }, + "io.k8s.kubernetes.pkg.api.v1.Volume": []CustomConstructorSpec{ + newConstructor( + "fromConfigMap", + newParam("name"), + newParamNestedRef("configMapName", "mixin.configMap.name"), + newParamNestedRef("configMapItems", "mixin.configMap.items")), + newConstructor( + "fromEmptyDir", + newParam("name"), + newParamNestedRefDefault("emptyDir", "mixin.emptyDir.mixinInstance", "{}")), + newConstructor( + "fromPersistentVolumeClaim", + newParam("name"), + newParamNestedRef("claimName", "mixin.persistentVolumeClaim.claimName")), + newConstructor( + "fromHostPath", + newParam("name"), + newParamNestedRef("hostPath", "mixin.hostPath.path")), + }, + "io.k8s.kubernetes.pkg.api.v1.VolumeMount": []CustomConstructorSpec{ + newConstructor("new", newParam("name"), newParam("mountPath"), newParamWithDefault("readOnly", "false")), + }, + "io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment": []CustomConstructorSpec{ + newConstructor( + "new", + newParamNestedRef("name", "mixin.metadata.name"), + newParamNestedRef("replicas", "mixin.spec.replicas"), + newParamNestedRef("containers", "mixin.spec.template.spec.containers"), + newParamNestedRefDefault("podLabels", "mixin.spec.template.metadata.labels", "{}")), + }, + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Deployment": []CustomConstructorSpec{ + newConstructor( + "new", + newParamNestedRef("name", "mixin.metadata.name"), + newParamNestedRef("replicas", "mixin.spec.replicas"), + newParamNestedRef("containers", "mixin.spec.template.spec.containers"), + newParamNestedRefDefault("podLabels", "mixin.spec.template.metadata.labels", "{}")), + }, + }, + propertyBlacklist: map[string]propertySet{ + // Metadata fields. + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": newPropertySet( + "creationTimestamp", "deletionTimestamp", "generation", + "ownerReferences", "resourceVersion", "selfLink", "uid", + ), + + // Fields whose types are + // `io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta`. + "io.k8s.kubernetes.pkg.api.v1.ComponentStatusList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.ConfigMapList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.EndpointsList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.EventList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.LimitRangeList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.NamespaceList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.NodeList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.PersistentVolumeClaimList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.PersistentVolumeList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.PodList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.PodTemplateList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.ReplicationControllerList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.ResourceQuotaList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.SecretList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.ServiceAccountList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.api.v1.ServiceList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.apps.v1beta1.StatefulSetList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.autoscaling.v1.HorizontalPodAutoscalerList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.autoscaling.v2alpha1.HorizontalPodAutoscalerList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.batch.v1.JobList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.batch.v2alpha1.CronJobList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.certificates.v1beta1.CertificateSigningRequestList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DaemonSetList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DeploymentList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.IngressList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.NetworkPolicyList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.PodSecurityPolicyList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ReplicaSetList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ThirdPartyResourceList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.policy.v1beta1.PodDisruptionBudgetList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.ClusterRoleBindingList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.ClusterRoleList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.RoleBindingList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1alpha1.RoleList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1beta1.ClusterRoleBindingList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1beta1.ClusterRoleList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1beta1.RoleBindingList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.rbac.v1beta1.RoleList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.settings.v1alpha1.PodPresetList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.storage.v1.StorageClassList": newPropertySet("metadata"), + "io.k8s.kubernetes.pkg.apis.storage.v1beta1.StorageClassList": newPropertySet("metadata"), + + // Status fields. + "io.k8s.kubernetes.pkg.api.v1.Namespace": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.Node": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.NodeCondition": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.PersistentVolume": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.PersistentVolumeClaim": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.Pod": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.PodCondition": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.ReplicationController": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.ReplicationControllerCondition": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.ResourceQuota": newPropertySet("status"), + "io.k8s.kubernetes.pkg.api.v1.Service": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.apps.v1beta1.DeploymentCondition": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.apps.v1beta1.Scale": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.apps.v1beta1.StatefulSet": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authentication.v1.TokenReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authentication.v1beta1.TokenReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authorization.v1.LocalSubjectAccessReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authorization.v1.SelfSubjectAccessReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authorization.v1.SubjectAccessReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authorization.v1beta1.LocalSubjectAccessReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authorization.v1beta1.SelfSubjectAccessReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.authorization.v1beta1.SubjectAccessReview": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.autoscaling.v1.HorizontalPodAutoscaler": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.autoscaling.v1.Scale": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.autoscaling.v2alpha1.HorizontalPodAutoscaler": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.batch.v1.Job": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.batch.v1.JobCondition": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.batch.v2alpha1.CronJob": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.certificates.v1beta1.CertificateSigningRequest": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DaemonSet": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Deployment": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DeploymentCondition": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Ingress": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ReplicaSet": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.ReplicaSetCondition": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.Scale": newPropertySet("status"), + "io.k8s.kubernetes.pkg.apis.policy.v1beta1.PodDisruptionBudget": newPropertySet("status"), + + // TODO: Find a more principled way to omit "status" types. + // Currently we emit these in the `local hidden` in the `root`, + // so that we can type aliases. To get around the fact that some + // of their function names collide with Jsonnet keywords, we + // simply choose not to emit them. Eventually we will approach + // this problem in a more principled manner. + "io.k8s.kubernetes.pkg.api.v1.ComponentCondition": newPropertySet("error", "status"), + "io.k8s.kubernetes.pkg.apis.authentication.v1.TokenReviewStatus": newPropertySet("error"), + "io.k8s.kubernetes.pkg.apis.authentication.v1beta1.TokenReviewStatus": newPropertySet("error"), + + // Has both status and a property with type + // `io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta`. + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": newPropertySet("status", "metadata"), + + // Misc. + "io.k8s.kubernetes.pkg.apis.extensions.v1beta1.DaemonSetSpec": newPropertySet("templateGeneration"), + }, + }, +} diff --git a/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/version.go b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/version.go new file mode 100644 index 0000000000000000000000000000000000000000..f988fcfeaf43f6c33cf433c8aa2c75f05adda88a --- /dev/null +++ b/vendor/github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion/version.go @@ -0,0 +1,183 @@ +// Package kubeversion contains a collection of helper methods that +// help to customize the code generated for ksonnet-lib to suit +// different Kubernetes versions. +// +// For example, we may choose not to emit certain properties for some +// objects in Kubernetes v1.7.0; or, we might want to rename a +// property method. This package contains both the helper methods that +// perform such transformations, as well as the data for the +// transformations we use for each version. +package kubeversion + +import ( + "log" + + "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec" +) + +// MapIdentifier takes a text identifier and maps it to a +// Jsonnet-appropriate identifier, for some version of Kubernetes. For +// example, in Kubernetes v1.7.0, we might map `clusterIP` -> +// `clusterIp`. +func MapIdentifier(k8sVersion, id string) string { + verData, ok := versions[k8sVersion] + if !ok { + log.Fatalf("Unrecognized Kubernetes version '%s'", k8sVersion) + } + + if alias, ok := verData.idAliases[id]; ok { + return alias + } + return id +} + +// IsBlacklistedProperty taks a definition name (e.g., +// `io.k8s.kubernetes.pkg.apis.apps.v1beta1.Deployment`), a property +// name (e.g., `status`), and reports whether it is blacklisted for +// some Kubernetes version. This is particularly useful when deciding +// whether or not to generate mixins and property methods for a given +// property (as we likely wouldn't in the case of, say, `status`). +func IsBlacklistedProperty( + k8sVersion string, path kubespec.DefinitionName, + propertyName kubespec.PropertyName, +) bool { + verData, ok := versions[k8sVersion] + if !ok { + return false + } + + bl, ok := verData.propertyBlacklist[string(path)] + if !ok { + return false + } + + _, ok = bl[string(propertyName)] + return ok +} + +func ConstructorSpec( + k8sVersion string, path kubespec.DefinitionName, +) ([]CustomConstructorSpec, bool) { + verData, ok := versions[k8sVersion] + if !ok { + log.Fatalf("Unrecognized Kubernetes version '%s'", k8sVersion) + } + + spec, ok := verData.constructorSpecs[string(path)] + return spec, ok +} + +//----------------------------------------------------------------------------- +// Core data structures for specifying version information. +//----------------------------------------------------------------------------- + +type versionData struct { + idAliases map[string]string + constructorSpecs map[string][]CustomConstructorSpec + propertyBlacklist map[string]propertySet +} + +type propertySet map[string]bool + +func newPropertySet(strings ...string) propertySet { + ps := make(propertySet) + for _, s := range strings { + ps[s] = true + } + + return ps +} + +//----------------------------------------------------------------------------- +// Public Data structures for specifying custom constructors for API +// objects. +//----------------------------------------------------------------------------- + +// CustomConstructorSpec specifies a custom constructor for +// `ksonnet-gen` to emit as part of ksonnet-lib. In particular, this +// specifies a constructor of the form: +// +// foo(bar, baz):: self.bar(bar) + self.baz(baz) +// +// The parameter list and the body are all generated from the `Params` +// field. +// +// DESIGN NOTES: +// +// * If the user specifies a custom constructor, we will not emit the +// default zero-argument constructor, `new()`. This is a purposeful +// decision which we make because we are typically customizing the +// constructors precisely because the zero-argument constructor is +// not meaninful for a given API object. +// * We currently do not check that parameter names are unique. +// Duplicate identifiers in a parameter list results in a Jsonnet +// compiler error, though, so this should be caught by review and +// CI, and it is hence not important for this case to be covered by +// this code. +type CustomConstructorSpec struct { + ID string + Params []CustomConstructorParam +} + +func newConstructor( + id string, params ...CustomConstructorParam, +) CustomConstructorSpec { + return CustomConstructorSpec{ + ID: id, + Params: params, + } +} + +// CustomConstructorParam specifies a parameter for a +// `CustomConstructorSpec`. This class allows users to specify +// constructors of various forms, including: +// +// * The "normal" form, e.g., `foo(bar):: self.bar(bar)`, +// * Parameters with default values, e.g., `foo(bar="baz"):: +// self.bar(bar)`, and +// * Parameters that are nested inside the object, e.g., `foo(bar):: +// self.baz.bat.bar(bar)` +// +// DESIGN NOTES: +// +// * For constructors that use nested paths, we do not currently check +// that the path is valid. So for example, `self.baz.bat.bar` in the +// example above may not correspond to a real property. We make this +// decision because it complicates the code, and it doesn't seem +// worth it since this feature is used relatively rarely. +type CustomConstructorParam struct { + ID string + DefaultValue *string + RelativePath *string +} + +func newParam(name string) CustomConstructorParam { + return CustomConstructorParam{ + ID: name, + DefaultValue: nil, + } +} + +func newParamWithDefault(name, def string) CustomConstructorParam { + return CustomConstructorParam{ + ID: name, + DefaultValue: &def, + } +} + +func newParamNestedRef(name, relativePath string) CustomConstructorParam { + return CustomConstructorParam{ + ID: name, + RelativePath: &relativePath, + } +} + +func newParamNestedRefDefault( + name, relativePath, def string, +) CustomConstructorParam { + return CustomConstructorParam{ + ID: name, + RelativePath: &relativePath, + DefaultValue: &def, + } +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 7e9f4892e7a5721a9ad2e57ad6c381a3cf5c20a5..35bf4820f25089705c1ba8539b5cabe94e820471 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -190,6 +190,30 @@ "revision": "acf38b000a03e4ab89e40f20f1e548f4e6ac7f72", "revisionTime": "2017-03-14T01:17:55Z" }, + { + "checksumSHA1": "wX+GmcWpMzCIcxR9YtN1FCM9BEE=", + "path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/jsonnet", + "revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722", + "revisionTime": "2017-08-16T22:14:57Z" + }, + { + "checksumSHA1": "LGk0N301rp2uQSOmy7kBQEksues=", + "path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/ksonnet", + "revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722", + "revisionTime": "2017-08-16T22:14:57Z" + }, + { + "checksumSHA1": "BiiHRiYpSOb+vHiP6h/A9lRotEY=", + "path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubespec", + "revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722", + "revisionTime": "2017-08-16T22:14:57Z" + }, + { + "checksumSHA1": "jpBOSqq1T9UyiANimAKtzyTt3qo=", + "path": "github.com/ksonnet/ksonnet-lib/ksonnet-gen/kubeversion", + "revision": "fcf4c31408afc7b7469a1722c4cc06ccc315a722", + "revisionTime": "2017-08-16T22:14:57Z" + }, { "checksumSHA1": "T8soMJArSZrYnhmdpAnq1bVxQ6Q=", "path": "github.com/mailru/easyjson/buffer",