diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index c0e4f7f5efe..cb85111db26 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -285,7 +285,8 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob NewCmdCreate(f, out, err), NewCmdExposeService(f, out), NewCmdRun(f, in, out, err), - set.NewCmdSet(f, out, err), + set.NewCmdSet(f, in, out, err), + deprecatedAlias("run-container", NewCmdRun(f, in, out, err)), }, }, { diff --git a/pkg/kubectl/cmd/set/BUILD b/pkg/kubectl/cmd/set/BUILD index 70f56c20c7f..32800b969e0 100644 --- a/pkg/kubectl/cmd/set/BUILD +++ b/pkg/kubectl/cmd/set/BUILD @@ -9,6 +9,7 @@ go_library( srcs = [ "helper.go", "set.go", + "set_env.go", "set_image.go", "set_resources.go", "set_selector.go", @@ -22,6 +23,7 @@ go_library( "//pkg/kubectl:go_default_library", "//pkg/kubectl/cmd/templates:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/kubectl/cmd/util/env:go_default_library", "//pkg/kubectl/resource:go_default_library", "//pkg/kubectl/util/i18n:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", @@ -40,6 +42,7 @@ go_library( go_test( name = "go_default_test", srcs = [ + "set_env_test.go", "set_image_test.go", "set_resources_test.go", "set_selector_test.go", diff --git a/pkg/kubectl/cmd/set/helper.go b/pkg/kubectl/cmd/set/helper.go index 35d49dc46e2..f85f2b1fcf1 100644 --- a/pkg/kubectl/cmd/set/helper.go +++ b/pkg/kubectl/cmd/set/helper.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/kubernetes/pkg/api" kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" @@ -158,3 +159,37 @@ func CalculatePatches(infos []*resource.Info, encoder runtime.Encoder, mutateFn } return patches } + +func findEnv(env []api.EnvVar, name string) (api.EnvVar, bool) { + for _, e := range env { + if e.Name == name { + return e, true + } + } + return api.EnvVar{}, false +} + +func updateEnv(existing []api.EnvVar, env []api.EnvVar, remove []string) []api.EnvVar { + out := []api.EnvVar{} + covered := sets.NewString(remove...) + for _, e := range existing { + if covered.Has(e.Name) { + continue + } + newer, ok := findEnv(env, e.Name) + if ok { + covered.Insert(e.Name) + out = append(out, newer) + continue + } + out = append(out, e) + } + for _, e := range env { + if covered.Has(e.Name) { + continue + } + covered.Insert(e.Name) + out = append(out, e) + } + return out +} diff --git a/pkg/kubectl/cmd/set/set.go b/pkg/kubectl/cmd/set/set.go index bef7e2dd3a8..e905c3fb5b7 100644 --- a/pkg/kubectl/cmd/set/set.go +++ b/pkg/kubectl/cmd/set/set.go @@ -32,7 +32,7 @@ var ( These commands help you make changes to existing application resources.`) ) -func NewCmdSet(f cmdutil.Factory, out, err io.Writer) *cobra.Command { +func NewCmdSet(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "set SUBCOMMAND", Short: i18n.T("Set specific features on objects"), @@ -46,5 +46,7 @@ func NewCmdSet(f cmdutil.Factory, out, err io.Writer) *cobra.Command { cmd.AddCommand(NewCmdSelector(f, out)) cmd.AddCommand(NewCmdSubject(f, out, err)) cmd.AddCommand(NewCmdServiceAccount(f, out, err)) + cmd.AddCommand(NewCmdEnv(f, in, out, err)) + return cmd } diff --git a/pkg/kubectl/cmd/set/set_env.go b/pkg/kubectl/cmd/set/set_env.go new file mode 100644 index 00000000000..321879eefd7 --- /dev/null +++ b/pkg/kubectl/cmd/set/set_env.go @@ -0,0 +1,430 @@ +/* +Copyright 2017 The Kubernetes 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 set + +import ( + "errors" + "fmt" + "io" + "regexp" + "sort" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + envutil "k8s.io/kubernetes/pkg/kubectl/cmd/util/env" + "k8s.io/kubernetes/pkg/kubectl/resource" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" +) + +var ( + envResources = ` + pod (po), replicationcontroller (rc), deployment (deploy), daemonset (ds), job, replicaset (rs)` + + envLong = templates.LongDesc(` + Update environment variables on a pod template. + + List environment variable definitions in one or more pods, pod templates. + Add, update, or remove container environment variable definitions in one or + more pod templates (within replication controllers or deployment configurations). + View or modify the environment variable definitions on all containers in the + specified pods or pod templates, or just those that match a wildcard. + + If "--env -" is passed, environment variables can be read from STDIN using the standard env + syntax. + + Possible resources include (case insensitive): + ` + envResources) + + envExample = templates.Examples(` + # Update deployment 'registry' with a new environment variable + kubectl set env deployment/registry STORAGE_DIR=/local + + # List the environment variables defined on a deployments 'sample-build' + kubectl set env deployment/sample-build --list + + # List the environment variables defined on all pods + kubectl set env pods --all --list + + # Output modified deployment in YAML, and does not alter the object on the server + kubectl set env deployment/sample-build STORAGE_DIR=/data -o yaml + + # Update all containers in all replication controllers in the project to have ENV=prod + kubectl set env rc --all ENV=prod + + # Import environment from a secret + kubectl set env --from=secret/mysecret dc/myapp + + # Import environment from a config map with a prefix + kubectl set env --from=configmap/myconfigmap --prefix=MYSQL_ dc/myapp + + # Remove the environment variable ENV from container 'c1' in all deployment configs + kubectl set env deployments --all --containers="c1" ENV- + + # Remove the environment variable ENV from a deployment definition on disk and + # update the deployment config on the server + kubectl set env -f deploy.json ENV- + + # Set some of the local shell environment into a deployment config on the server + env | grep RAILS_ | kubectl set env -e - dc/registry`) +) + +type EnvOptions struct { + Out io.Writer + Err io.Writer + In io.Reader + + resource.FilenameOptions + EnvParams []string + EnvArgs []string + Resources []string + + All bool + Resolve bool + List bool + ShortOutput bool + Local bool + Overwrite bool + DryRun bool + + ResourceVersion string + ContainerSelector string + Selector string + Output string + From string + Prefix string + + Mapper meta.RESTMapper + Typer runtime.ObjectTyper + Builder *resource.Builder + Infos []*resource.Info + Encoder runtime.Encoder + + Cmd *cobra.Command + + UpdatePodSpecForObject func(obj runtime.Object, fn func(*api.PodSpec) error) (bool, error) + PrintObject func(cmd *cobra.Command, isLocal bool, mapper meta.RESTMapper, obj runtime.Object, out io.Writer) error +} + +// NewCmdEnv implements the OpenShift cli env command +func NewCmdEnv(f cmdutil.Factory, in io.Reader, out, errout io.Writer) *cobra.Command { + options := &EnvOptions{ + Out: out, + Err: errout, + In: in, + } + cmd := &cobra.Command{ + Use: "env RESOURCE/NAME KEY_1=VAL_1 ... KEY_N=VAL_N", + Short: "Update environment variables on a pod template", + Long: envLong, + Example: fmt.Sprintf(envExample), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(options.Complete(f, cmd, args)) + cmdutil.CheckErr(options.RunEnv(f)) + }, + } + usage := "the resource to update the env" + cmdutil.AddFilenameOptionFlags(cmd, &options.FilenameOptions, usage) + cmd.Flags().StringVarP(&options.ContainerSelector, "containers", "c", "*", "The names of containers in the selected pod templates to change - may use wildcards") + cmd.Flags().StringP("from", "", "", "The name of a resource from which to inject environment variables") + cmd.Flags().StringP("prefix", "", "", "Prefix to append to variable names") + cmd.Flags().StringArrayVarP(&options.EnvParams, "env", "e", options.EnvParams, "Specify a key-value pair for an environment variable to set into each container.") + cmd.Flags().BoolVar(&options.List, "list", options.List, "If true, display the environment and any changes in the standard format") + cmd.Flags().BoolVar(&options.Resolve, "resolve", options.Resolve, "If true, show secret or configmap references when listing variables") + cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on") + cmd.Flags().BoolVar(&options.Local, "local", false, "If true, set image will NOT contact api-server but run locally.") + cmd.Flags().BoolVar(&options.All, "all", options.All, "If true, select all resources in the namespace of the specified resource types") + cmd.Flags().BoolVar(&options.Overwrite, "overwrite", true, "If true, allow environment to be overwritten, otherwise reject updates that overwrite existing environment.") + + cmdutil.AddDryRunFlag(cmd) + cmdutil.AddPrinterFlags(cmd) + + return cmd +} + +func validateNoOverwrites(existing []api.EnvVar, env []api.EnvVar) error { + for _, e := range env { + if current, exists := findEnv(existing, e.Name); exists && current.Value != e.Value { + return fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", current.Name, current.Value) + } + } + return nil +} + +func keyToEnvName(key string) string { + validEnvNameRegexp := regexp.MustCompile("[^a-zA-Z0-9_]") + return strings.ToUpper(validEnvNameRegexp.ReplaceAllString(key, "_")) +} + +func (o *EnvOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + resources, envArgs, ok := envutil.SplitEnvironmentFromResources(args) + if !ok { + return cmdutil.UsageErrorf(o.Cmd, "all resources must be specified before environment changes: %s", strings.Join(args, " ")) + } + if len(o.Filenames) == 0 && len(resources) < 1 { + return cmdutil.UsageErrorf(cmd, "one or more resources must be specified as or /") + } + + o.Mapper, o.Typer = f.Object() + o.UpdatePodSpecForObject = f.UpdatePodSpecForObject + o.Encoder = f.JSONEncoder() + o.ContainerSelector = cmdutil.GetFlagString(cmd, "containers") + o.List = cmdutil.GetFlagBool(cmd, "list") + o.Resolve = cmdutil.GetFlagBool(cmd, "resolve") + o.Selector = cmdutil.GetFlagString(cmd, "selector") + o.All = cmdutil.GetFlagBool(cmd, "all") + o.Overwrite = cmdutil.GetFlagBool(cmd, "overwrite") + o.Output = cmdutil.GetFlagString(cmd, "output") + o.From = cmdutil.GetFlagString(cmd, "from") + o.Prefix = cmdutil.GetFlagString(cmd, "prefix") + o.DryRun = cmdutil.GetDryRunFlag(cmd) + o.PrintObject = f.PrintObject + + o.EnvArgs = envArgs + o.Resources = resources + o.Cmd = cmd + + o.ShortOutput = cmdutil.GetFlagString(cmd, "output") == "name" + + if o.List && len(o.Output) > 0 { + return cmdutil.UsageErrorf(o.Cmd, "--list and --output may not be specified together") + } + + return nil +} + +// RunEnv contains all the necessary functionality for the OpenShift cli env command +func (o *EnvOptions) RunEnv(f cmdutil.Factory) error { + kubeClient, err := f.ClientSet() + if err != nil { + return err + } + + cmdNamespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + + env, remove, err := envutil.ParseEnv(append(o.EnvParams, o.EnvArgs...), o.In) + if err != nil { + return err + } + + if len(o.From) != 0 { + b := f.NewBuilder(!o.Local). + ContinueOnError(). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, &o.FilenameOptions). + Flatten() + + if !o.Local { + b = b. + SelectorParam(o.Selector). + ResourceTypeOrNameArgs(o.All, o.From). + Latest() + } + + infos, err := b.Do().Infos() + if err != nil { + return err + } + + for _, info := range infos { + switch from := info.Object.(type) { + case *api.Secret: + for key := range from.Data { + envVar := api.EnvVar{ + Name: keyToEnvName(key), + ValueFrom: &api.EnvVarSource{ + SecretKeyRef: &api.SecretKeySelector{ + LocalObjectReference: api.LocalObjectReference{ + Name: from.Name, + }, + Key: key, + }, + }, + } + env = append(env, envVar) + } + case *api.ConfigMap: + for key := range from.Data { + envVar := api.EnvVar{ + Name: keyToEnvName(key), + ValueFrom: &api.EnvVarSource{ + ConfigMapKeyRef: &api.ConfigMapKeySelector{ + LocalObjectReference: api.LocalObjectReference{ + Name: from.Name, + }, + Key: key, + }, + }, + } + env = append(env, envVar) + } + default: + return fmt.Errorf("unsupported resource specified in --from") + } + } + } + + if len(o.Prefix) != 0 { + for i := range env { + env[i].Name = fmt.Sprintf("%s%s", o.Prefix, env[i].Name) + } + } + + b := f.NewBuilder(!o.Local). + ContinueOnError(). + NamespaceParam(cmdNamespace).DefaultNamespace(). + FilenameParam(enforceNamespace, &o.FilenameOptions). + Flatten() + + if !o.Local { + b = b. + SelectorParam(o.Selector). + ResourceTypeOrNameArgs(o.All, o.Resources...). + Latest() + } + + o.Infos, err = b.Do().Infos() + if err != nil { + return err + } + patches := CalculatePatches(o.Infos, o.Encoder, func(info *resource.Info) ([]byte, error) { + _, err := f.UpdatePodSpecForObject(info.Object, func(spec *api.PodSpec) error { + resolutionErrorsEncountered := false + containers, _ := selectContainers(spec.Containers, o.ContainerSelector) + if len(containers) == 0 { + fmt.Fprintf(o.Err, "warning: %s/%s does not have any containers matching %q\n", info.Mapping.Resource, info.Name, o.ContainerSelector) + return nil + } + for _, c := range containers { + if !o.Overwrite { + if err := validateNoOverwrites(c.Env, env); err != nil { + return err + } + } + + c.Env = updateEnv(c.Env, env, remove) + if o.List { + resolveErrors := map[string][]string{} + store := envutil.NewResourceStore() + + fmt.Fprintf(o.Out, "# %s %s, container %s\n", info.Mapping.Resource, info.Name, c.Name) + for _, env := range c.Env { + // Print the simple value + if env.ValueFrom == nil { + fmt.Fprintf(o.Out, "%s=%s\n", env.Name, env.Value) + continue + } + + // Print the reference version + if !o.Resolve { + fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom)) + continue + } + + value, err := envutil.GetEnvVarRefValue(kubeClient, cmdNamespace, store, env.ValueFrom, info.Object, c) + // Print the resolved value + if err == nil { + fmt.Fprintf(o.Out, "%s=%s\n", env.Name, value) + continue + } + + // Print the reference version and save the resolve error + fmt.Fprintf(o.Out, "# %s from %s\n", env.Name, envutil.GetEnvVarRefString(env.ValueFrom)) + errString := err.Error() + resolveErrors[errString] = append(resolveErrors[errString], env.Name) + resolutionErrorsEncountered = true + } + + // Print any resolution errors + errs := []string{} + for err, vars := range resolveErrors { + sort.Strings(vars) + errs = append(errs, fmt.Sprintf("error retrieving reference for %s: %v", strings.Join(vars, ", "), err)) + } + sort.Strings(errs) + for _, err := range errs { + fmt.Fprintln(o.Err, err) + } + } + } + if resolutionErrorsEncountered { + return errors.New("failed to retrieve valueFrom references") + } + return nil + }) + + if err == nil { + return runtime.Encode(o.Encoder, info.Object) + } + return nil, err + }) + + if o.List { + return nil + } + + allErrs := []error{} + + for _, patch := range patches { + info := patch.Info + if patch.Err != nil { + allErrs = append(allErrs, fmt.Errorf("error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err)) + continue + } + + // no changes + if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { + continue + } + + if o.PrintObject != nil && (o.Local || o.DryRun) { + if err := o.PrintObject(o.Cmd, o.Local, o.Mapper, info.Object, o.Out); err != nil { + return err + } + continue + } + + obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch) + if err != nil { + allErrs = append(allErrs, fmt.Errorf("failed to patch env update to pod template: %v\n", err)) + continue + } + info.Refresh(obj, true) + + // make sure arguments to set or replace environment variables are set + // before returning a successful message + if len(env) == 0 && len(o.EnvArgs) == 0 { + return fmt.Errorf("at least one environment variable must be provided") + } + + if len(o.Output) > 0 { + return o.PrintObject(o.Cmd, o.Local, o.Mapper, obj, o.Out) + } + + cmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, false, "env updated") + } + return utilerrors.NewAggregate(allErrs) +} diff --git a/pkg/kubectl/cmd/set/set_env_test.go b/pkg/kubectl/cmd/set/set_env_test.go new file mode 100644 index 00000000000..129a5c98c6e --- /dev/null +++ b/pkg/kubectl/cmd/set/set_env_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2017 The Kubernetes 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 set + +import ( + "bytes" + "net/http" + "strings" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/rest/fake" + "k8s.io/kubernetes/pkg/api" + cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/printers" + "os" +) + +func TestSetEnvLocal(t *testing.T) { + f, tf, codec, ns := cmdtesting.NewAPIFactory() + tf.Client = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req) + return nil, nil + }), + } + tf.Namespace = "test" + tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &api.Registry.GroupOrDie(api.GroupName).GroupVersion}} + + buf := bytes.NewBuffer([]byte{}) + cmd := NewCmdEnv(f, os.Stdin, buf, buf) + cmd.SetOutput(buf) + cmd.Flags().Set("output", "name") + cmd.Flags().Set("local", "true") + mapper, typer := f.Object() + tf.Printer = &printers.NamePrinter{Decoders: []runtime.Decoder{codec}, Typer: typer, Mapper: mapper} + + opts := EnvOptions{FilenameOptions: resource.FilenameOptions{ + Filenames: []string{"../../../../examples/storage/cassandra/cassandra-controller.yaml"}}, + Out: buf, + Local: true} + err := opts.Complete(f, cmd, []string{"env=prod"}) + if err == nil { + err = opts.RunEnv(f) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(buf.String(), "replicationcontrollers/cassandra") { + t.Errorf("did not set env: %s", buf.String()) + } +} diff --git a/pkg/kubectl/cmd/set/set_test.go b/pkg/kubectl/cmd/set/set_test.go index c0a889c15c5..1c7b42db060 100644 --- a/pkg/kubectl/cmd/set/set_test.go +++ b/pkg/kubectl/cmd/set/set_test.go @@ -23,13 +23,14 @@ import ( "github.com/spf13/cobra" clientcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "os" ) func TestLocalAndDryRunFlags(t *testing.T) { out := &bytes.Buffer{} errout := &bytes.Buffer{} f := clientcmdutil.NewFactory(nil) - setCmd := NewCmdSet(f, out, errout) + setCmd := NewCmdSet(f, os.Stdin, out, errout) ensureLocalAndDryRunFlagsOnChildren(t, setCmd, "") } diff --git a/pkg/kubectl/cmd/util/BUILD b/pkg/kubectl/cmd/util/BUILD index fb0f4c184b9..180988c990d 100644 --- a/pkg/kubectl/cmd/util/BUILD +++ b/pkg/kubectl/cmd/util/BUILD @@ -142,6 +142,7 @@ filegroup( srcs = [ ":package-srcs", "//pkg/kubectl/cmd/util/editor:all-srcs", + "//pkg/kubectl/cmd/util/env:all-srcs", "//pkg/kubectl/cmd/util/jsonmerge:all-srcs", "//pkg/kubectl/cmd/util/openapi:all-srcs", "//pkg/kubectl/cmd/util/sanity:all-srcs", diff --git a/pkg/kubectl/cmd/util/env/BUILD b/pkg/kubectl/cmd/util/env/BUILD new file mode 100644 index 00000000000..d918ecf6355 --- /dev/null +++ b/pkg/kubectl/cmd/util/env/BUILD @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "env_parse.go", + "env_resolve.go", + ], + visibility = ["//visibility:public"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/api/resource:go_default_library", + "//pkg/client/clientset_generated/internalclientset:go_default_library", + "//pkg/fieldpath:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/pkg/kubectl/cmd/util/env/env_parse.go b/pkg/kubectl/cmd/util/env/env_parse.go new file mode 100644 index 00000000000..d9c15123dc2 --- /dev/null +++ b/pkg/kubectl/cmd/util/env/env_parse.go @@ -0,0 +1,154 @@ +/* +Copyright 2017 The Kubernetes 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 env + +import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/kubernetes/pkg/api" +) + +// Env returns an environment variable or a default value if not specified. +func Env(key string, defaultValue string) string { + val := os.Getenv(key) + if len(val) == 0 { + return defaultValue + } + return val +} + +// GetEnv returns an environment value if specified +func GetEnv(key string) (string, bool) { + val := os.Getenv(key) + if len(val) == 0 { + return "", false + } + return val, true +} + +var argumentEnvironment = regexp.MustCompile("(?ms)^(.+)\\=(.*)$") +var validArgumentEnvironment = regexp.MustCompile("(?ms)^(\\w+)\\=(.*)$") + +// IsEnvironmentArgument check str is env args +func IsEnvironmentArgument(s string) bool { + return argumentEnvironment.MatchString(s) +} + +// IsValidEnvironmentArgument check str is valid env +func IsValidEnvironmentArgument(s string) bool { + return validArgumentEnvironment.MatchString(s) +} + +// SplitEnvironmentFromResources returns resources and envargs +func SplitEnvironmentFromResources(args []string) (resources, envArgs []string, ok bool) { + first := true + for _, s := range args { + // this method also has to understand env removal syntax, i.e. KEY- + isEnv := IsEnvironmentArgument(s) || strings.HasSuffix(s, "-") + switch { + case first && isEnv: + first = false + fallthrough + case !first && isEnv: + envArgs = append(envArgs, s) + case first && !isEnv: + resources = append(resources, s) + case !first && !isEnv: + return nil, nil, false + } + } + return resources, envArgs, true +} + +// parseIntoEnvVar parses the list of key-value pairs into kubernetes EnvVar. +// envVarType is for making errors more specific to user intentions. +func parseIntoEnvVar(spec []string, defaultReader io.Reader, envVarType string) ([]api.EnvVar, []string, error) { + env := []api.EnvVar{} + exists := sets.NewString() + var remove []string + for _, envSpec := range spec { + switch { + case !IsValidEnvironmentArgument(envSpec) && !strings.HasSuffix(envSpec, "-"): + return nil, nil, fmt.Errorf("%ss must be of the form key=value and can only contain letters, numbers, and underscores", envVarType) + case envSpec == "-": + if defaultReader == nil { + return nil, nil, fmt.Errorf("when '-' is used, STDIN must be open") + } + fileEnv, err := readEnv(defaultReader, envVarType) + if err != nil { + return nil, nil, err + } + env = append(env, fileEnv...) + case strings.Index(envSpec, "=") != -1: + parts := strings.SplitN(envSpec, "=", 2) + if len(parts) != 2 { + return nil, nil, fmt.Errorf("invalid %s: %v", envVarType, envSpec) + } + exists.Insert(parts[0]) + env = append(env, api.EnvVar{ + Name: parts[0], + Value: parts[1], + }) + case strings.HasSuffix(envSpec, "-"): + remove = append(remove, envSpec[:len(envSpec)-1]) + default: + return nil, nil, fmt.Errorf("unknown %s: %v", envVarType, envSpec) + } + } + for _, removeLabel := range remove { + if _, found := exists[removeLabel]; found { + return nil, nil, fmt.Errorf("can not both modify and remove the same %s in the same command", envVarType) + } + } + return env, remove, nil +} + +// ParseEnv parse env from reader +func ParseEnv(spec []string, defaultReader io.Reader) ([]api.EnvVar, []string, error) { + return parseIntoEnvVar(spec, defaultReader, "environment variable") +} + +func readEnv(r io.Reader, envVarType string) ([]api.EnvVar, error) { + env := []api.EnvVar{} + scanner := bufio.NewScanner(r) + for scanner.Scan() { + envSpec := scanner.Text() + if pos := strings.Index(envSpec, "#"); pos != -1 { + envSpec = envSpec[:pos] + } + if strings.Index(envSpec, "=") != -1 { + parts := strings.SplitN(envSpec, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid %s: %v", envVarType, envSpec) + } + env = append(env, api.EnvVar{ + Name: parts[0], + Value: parts[1], + }) + } + } + if err := scanner.Err(); err != nil && err != io.EOF { + return nil, err + } + return env, nil +} diff --git a/pkg/kubectl/cmd/util/env/env_resolve.go b/pkg/kubectl/cmd/util/env/env_resolve.go new file mode 100644 index 00000000000..8955fb21999 --- /dev/null +++ b/pkg/kubectl/cmd/util/env/env_resolve.go @@ -0,0 +1,133 @@ +/* +Copyright 2017 The Kubernetes 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 env + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" + clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/fieldpath" +) + +// ResourceStore defines a new resource store data structure +type ResourceStore struct { + SecretStore map[string]*api.Secret + ConfigMapStore map[string]*api.ConfigMap +} + +// NewResourceStore returns a pointer to a new resource store data structure +func NewResourceStore() *ResourceStore { + return &ResourceStore{ + SecretStore: make(map[string]*api.Secret), + ConfigMapStore: make(map[string]*api.ConfigMap), + } +} + +// getSecretRefValue returns the value of a secret in the supplied namespace +func getSecretRefValue(client clientset.Interface, namespace string, store *ResourceStore, secretSelector *api.SecretKeySelector) (string, error) { + secret, ok := store.SecretStore[secretSelector.Name] + if !ok { + var err error + secret, err = client.Core().Secrets(namespace).Get(secretSelector.Name, metav1.GetOptions{}) + if err != nil { + return "", err + } + store.SecretStore[secretSelector.Name] = secret + } + if data, ok := secret.Data[secretSelector.Key]; ok { + return string(data), nil + } + return "", fmt.Errorf("key %s not found in secret %s", secretSelector.Key, secretSelector.Name) + +} + +// getConfigMapRefValue returns the value of a configmap in the supplied namespace +func getConfigMapRefValue(client clientset.Interface, namespace string, store *ResourceStore, configMapSelector *api.ConfigMapKeySelector) (string, error) { + configMap, ok := store.ConfigMapStore[configMapSelector.Name] + if !ok { + var err error + configMap, err = client.Core().ConfigMaps(namespace).Get(configMapSelector.Name, metav1.GetOptions{}) + if err != nil { + return "", err + } + store.ConfigMapStore[configMapSelector.Name] = configMap + } + if data, ok := configMap.Data[configMapSelector.Key]; ok { + return string(data), nil + } + return "", fmt.Errorf("key %s not found in config map %s", configMapSelector.Key, configMapSelector.Name) +} + +// getFieldRef returns the value of the supplied path in the given object +func getFieldRef(obj runtime.Object, from *api.EnvVarSource) (string, error) { + return fieldpath.ExtractFieldPathAsString(obj, from.FieldRef.FieldPath) +} + +// getResourceFieldRef returns the value of a resource in the given container +func getResourceFieldRef(from *api.EnvVarSource, c *api.Container) (string, error) { + return resource.ExtractContainerResourceValue(from.ResourceFieldRef, c) +} + +// GetEnvVarRefValue returns the value referenced by the supplied envvarsource given the other supplied information +func GetEnvVarRefValue(kc clientset.Interface, ns string, store *ResourceStore, from *api.EnvVarSource, obj runtime.Object, c *api.Container) (string, error) { + if from.SecretKeyRef != nil { + return getSecretRefValue(kc, ns, store, from.SecretKeyRef) + } + + if from.ConfigMapKeyRef != nil { + return getConfigMapRefValue(kc, ns, store, from.ConfigMapKeyRef) + } + + if from.FieldRef != nil { + return getFieldRef(obj, from) + } + + if from.ResourceFieldRef != nil { + return getResourceFieldRef(from, c) + } + + return "", fmt.Errorf("invalid valueFrom") +} + +// GetEnvVarRefString returns a text description of the supplied envvarsource +func GetEnvVarRefString(from *api.EnvVarSource) string { + if from.ConfigMapKeyRef != nil { + return fmt.Sprintf("configmap %s, key %s", from.ConfigMapKeyRef.Name, from.ConfigMapKeyRef.Key) + } + + if from.SecretKeyRef != nil { + return fmt.Sprintf("secret %s, key %s", from.SecretKeyRef.Name, from.SecretKeyRef.Key) + } + + if from.FieldRef != nil { + return fmt.Sprintf("field path %s", from.FieldRef.FieldPath) + } + + if from.ResourceFieldRef != nil { + containerPrefix := "" + if from.ResourceFieldRef.ContainerName != "" { + containerPrefix = fmt.Sprintf("%s/", from.ResourceFieldRef.ContainerName) + } + return fmt.Sprintf("resource field %s%s", containerPrefix, from.ResourceFieldRef.Resource) + } + + return "invalid valueFrom" +}