// Copyright 2020 The Knative 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 flags import ( "errors" "fmt" "strings" corev1 "k8s.io/api/core/v1" "knative.dev/client/pkg/util" "github.com/spf13/pflag" ) // PodSpecFlags to hold the container resource requirements values type PodSpecFlags struct { // Direct field manipulation Image uniqueStringArg ImagePullPolicy string Env []string EnvFrom []string EnvValueFrom []string EnvFile string Mount []string Volume []string NodeSelector []string Toleration []string NodeAffinity []string Command []string Arg []string LivenessProbe string LivenessProbeOpts string ReadinessProbe string ReadinessProbeOpts string ExtraContainers string Resources ResourceOptions Port string ServiceAccountName string ImagePullSecrets string User int64 SecurityContext string } type ResourceFlags struct { CPU string Memory string } // -- uniqueStringArg Value // Custom implementation of flag.Value interface to prevent multiple value assignment. // Useful to enforce unique use of flags, e.g. --image. type uniqueStringArg string func (s *uniqueStringArg) Set(val string) error { if len(*s) > 0 { return errors.New("can be provided only once") } *s = uniqueStringArg(val) return nil } func (s *uniqueStringArg) Type() string { return "string" } func (s *uniqueStringArg) String() string { return string(*s) } // AddUpdateFlags will add PodSpec flags related to environment variable to FlagSet of update command func (p *PodSpecFlags) AddUpdateFlags(flagset *pflag.FlagSet) []string { flagNames := []string{} flagset.StringArrayVarP(&p.Env, "env", "e", []string{}, "Environment variable to set. NAME=value; you may provide this flag "+ "any number of times to set multiple environment variables. "+ "To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).") flagNames = append(flagNames, "env") flagset.StringArrayVarP(&p.EnvValueFrom, "env-value-from", "", []string{}, "Add environment variable from a value of key in ConfigMap (prefix cm: or config-map:) or a Secret (prefix sc: or secret:). "+ "Example: --env-value-from NAME=cm:myconfigmap:key or --env-value-from NAME=secret:mysecret:key. "+ "You can use this flag multiple times. "+ "To unset a value from a ConfigMap/Secret key reference, append \"-\" to the key, e.g. --env-value-from ENV-.") flagNames = append(flagNames, "env-value-from") flagset.StringArrayVarP(&p.EnvFrom, "env-from", "", []string{}, "Add environment variables from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret:). "+ "Example: --env-from cm:myconfigmap or --env-from secret:mysecret. "+ "You can use this flag multiple times. "+ "To unset a ConfigMap/Secret reference, append \"-\" to the name, e.g. --env-from cm:myconfigmap-.") flagNames = append(flagNames, "env-from") return flagNames } // AddCreateFlags will add PodSpec flags related to environment variable to FlagSet of create command func (p *PodSpecFlags) AddCreateFlags(flagset *pflag.FlagSet) []string { flagNames := []string{} flagset.StringArrayVarP(&p.Env, "env", "e", []string{}, "Environment variable to set. NAME=value; you may provide this flag "+ "any number of times to set multiple environment variables.") flagNames = append(flagNames, "env") flagset.StringArrayVarP(&p.EnvValueFrom, "env-value-from", "", []string{}, "Add environment variable from a value of key in ConfigMap (prefix cm: or config-map:) or a Secret (prefix sc: or secret:). "+ "Example: --env-value-from NAME=cm:myconfigmap:key or --env-value-from NAME=secret:mysecret:key. "+ "You can use this flag multiple times.") flagNames = append(flagNames, "env-value-from") flagset.StringArrayVarP(&p.EnvFrom, "env-from", "", []string{}, "Add environment variables from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret:). "+ "Example: --env-from cm:myconfigmap or --env-from secret:mysecret. "+ "You can use this flag multiple times.") flagNames = append(flagNames, "env-from") return flagNames } // AddFlags will add PodSpec related flags to FlagSet func (p *PodSpecFlags) AddFlags(flagset *pflag.FlagSet) []string { flagNames := []string{} flagset.VarP(&p.Image, "image", "", "Image to run.") flagNames = append(flagNames, "image") flagset.StringVar(&p.ImagePullPolicy, "pull-policy", "", "Image pull policy. Valid values (case insensitive): Always | Never | IfNotPresent") flagset.StringVarP(&p.EnvFile, "env-file", "", "", "Path to a file containing environment variables (e.g. --env-file=/home/knative/service1/env).") flagNames = append(flagNames, "env-file") flagset.StringArrayVarP(&p.Mount, "mount", "", []string{}, "Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), "+ "a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. "+ "Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol "+ "or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is "+ "automatically generated. You can mount a volume with readOnly config (true | false) also. "+ "Example: --mount /mydir=ed:ed1:readOnly=true. "+ "You can specify a volume subpath by following the volume name with slash separated path. "+ "Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. "+ "You can use this flag multiple times. "+ "For unmounting a directory, append \"-\", e.g. --mount /mydir-, which also removes any auto-generated volume.") flagNames = append(flagNames, "mount") flagset.StringArrayVarP(&p.Volume, "volume", "", []string{}, "Add a volume from a ConfigMap (prefix cm: or config-map:) a Secret (prefix secret: or sc:), "+ "an EmptyDir (prefix ed: or emptyDir:) or a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim). "+ "PersistentVolumeClaim only works if the feature gate is enabled in Knative Serving feature flags configuration. Example: --volume myvolume=cm:myconfigmap, --volume myvolume=secret:mysecret or --volume emptyDir:myvol:size=1Gi,type=Memory. "+ "You can use this flag multiple times. "+ "To unset a ConfigMap/Secret reference, append \"-\" to the name, e.g. --volume myvolume-.") flagNames = append(flagNames, "volume") flagset.StringArrayVarP(&p.Command, "cmd", "", []string{}, "Specify command to be used as entrypoint instead of default one. "+ "Example: --cmd /app/start or --cmd sh --cmd /app/start.sh or --cmd /app/start --arg myArg to pass additional arguments.") flagNames = append(flagNames, "cmd") flagset.StringArrayVarP(&p.Arg, "arg", "", []string{}, "Add argument to the container command. "+ "Example: --arg myArg1 --arg --myArg2 --arg myArg3=3. "+ "You can use this flag multiple times.") flagNames = append(flagNames, "arg") // DEPRECATED since 1.0 flagset.StringVarP(&p.ExtraContainers, "extra-containers", "", "", "Deprecated, use --containers instead.") flagset.MarkHidden("extra-containers") flagNames = append(flagNames, "containers") flagset.StringVarP(&p.ExtraContainers, "containers", "", "", "Specify path to file including definition for additional containers, alternatively use '-' to read from stdin. "+ "Example: --containers ./containers.yaml or --containers -.") flagNames = append(flagNames, "containers") // Probes commonProbeDescription := "Supported probe types are HTTGet, Exec and TCPSocket. " + "Format: [http,https]:host:port:path, exec:cmd[,cmd,...], tcp:host:port." commonProbeOptsDesc := "Common opts (comma separated, case insensitive): InitialDelaySeconds=, FailureThreshold=, " + "SuccessThreshold=, PeriodSeconds=, TimeoutSeconds=" flagset.StringVarP(&p.LivenessProbe, "probe-liveness", "", "", "Add liveness probe to Service deployment. "+ commonProbeDescription) flagNames = append(flagNames, "probe-liveness") flagset.StringVarP(&p.LivenessProbeOpts, "probe-liveness-opts", "", "", "Add common options to liveness probe. "+ commonProbeOptsDesc) flagNames = append(flagNames, "probe-liveness-opts") flagset.StringVarP(&p.ReadinessProbe, "probe-readiness", "", "", "Add readiness probe to Service deployment. "+ commonProbeDescription) flagNames = append(flagNames, "probe-readiness") flagset.StringVarP(&p.ReadinessProbeOpts, "probe-readiness-opts", "", "", "Add common options to readiness probe. "+ commonProbeOptsDesc) flagNames = append(flagNames, "probe-liveness-opts") flagset.StringSliceVar(&p.Resources.Limits, "limit", nil, "The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. "+ "You can use this flag multiple times. "+ "To unset a resource limit, append \"-\" to the resource name, e.g. '--limit memory-'.") flagNames = append(flagNames, "limit") flagset.StringSliceVar(&p.Resources.Requests, "request", nil, "The resource requirement requests for this Service. For example, 'cpu=100m,memory=256Mi'. "+ "You can use this flag multiple times. "+ "To unset a resource request, append \"-\" to the resource name, e.g. '--request cpu-'.") flagNames = append(flagNames, "request") flagset.StringVarP(&p.Port, "port", "p", "", "The port where application listens on, in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'.") flagNames = append(flagNames, "port") flagset.StringVar(&p.ServiceAccountName, "service-account", "", "Service account name to set. An empty argument (\"\") clears the service account. The referenced service account must exist in the service's namespace.") flagNames = append(flagNames, "service-account") flagset.StringVar(&p.ImagePullSecrets, "pull-secret", "", "Image pull secret to set. An empty argument (\"\") clears the pull secret. The referenced secret must exist in the service's namespace.") flagNames = append(flagNames, "pull-secret") flagset.Int64VarP(&p.User, "user", "", 0, "The user ID to run the container (e.g., 1001).") flagNames = append(flagNames, "user") flagset.StringVar(&p.SecurityContext, "security-context", "none", "Predefined security context for the service. Accepted values: 'none' for no security context "+ "and 'strict' for dropping all capabilities, running as non-root, and no privilege escalation.") flagNames = append(flagNames, "security-context") flagset.StringArrayVar(&p.NodeSelector, "node-selector", []string{}, "Add node selector to be set, you may provide this flag any number of times to set multiple node selectors, "+ "works if feature flag is enabled in Knative Serving feature flags configuration. Example: --node-selector Disktype=\"ssd\". To unset, specify the key name followed by a \"-\", example: --node-selector Disktype- .") flagNames = append(flagNames, "node-selector") flagset.StringSliceVar(&p.Toleration, "toleration", []string{}, "Add toleration to be set, works if the feature gate is enabled in Knative Serving feature flags configuration. Example: "+ "--tolerations Key=\"key1\",Operator=\"Equal\",Value=\"value1\",Effect=\"NoSchedule\"") flagNames = append(flagNames, "toleration") flagset.StringSliceVar(&p.NodeAffinity, "node-affinity", []string{}, "Add node affinity to be set - only works if the feature gate is enabled in Knative Serving feature flags configuration. When key, operator, values (whitespace separated) and weight are defined for a type, they will be appended in nodeSelectorTerms in case of Required clause, "+ "implying the terms will be ORed, and for Preferred clause, all of them will be added in preferredDuringSchedulingIgnoredDuringExecution. Example: "+ "--node-affinity Type=\"Required\",Key=\"topology.kubernetes.io/zone\",Operator=\"In\",Values=\"antarctica-east1 antarctica-west1\" or "+ "--node-affinity Type=\"Preferred\",Key=\"topology.kubernetes.io/zone\",Operator=\"In\",Values=\"antarctica-east1\",Weight=\"1\"") flagNames = append(flagNames, "node-affinity") return flagNames } // ResolvePodSpec will create corev1.PodSpec based on the flag inputs and all input arguments func (p *PodSpecFlags) ResolvePodSpec(podSpec *corev1.PodSpec, flags *pflag.FlagSet, allArgs []string) error { var err error if flags.Changed("env") || flags.Changed("env-value-from") || flags.Changed("env-file") { envToUpdate, envToRemove, err := util.OrderedMapAndRemovalListFromArray(p.Env, "=") if err != nil { return fmt.Errorf("Invalid --env: %w", err) } envValueFromToUpdate, envValueFromToRemove, err := util.OrderedMapAndRemovalListFromArray(p.EnvValueFrom, "=") if err != nil { return fmt.Errorf("Invalid --env-value-from: %w", err) } envsFileToUpdate := util.NewOrderedMap() envsFileToRemove := []string{} if p.EnvFile != "" { envsFromFile, err := util.GetEnvsFromFile(p.EnvFile, "=") if err != nil { return fmt.Errorf("Invalid --env-file: %w", err) } envsFileToUpdate, envsFileToRemove, err = util.OrderedMapAndRemovalListFromArray(envsFromFile, "=") if err != nil { return fmt.Errorf("Invalid --env: %w", err) } } err = UpdateEnvVars( podSpec, allArgs, envToUpdate, envToRemove, envValueFromToUpdate, envValueFromToRemove, p.EnvFile, envsFileToUpdate, envsFileToRemove, ) if err != nil { return err } } if flags.Changed("env-from") { envFromSourceToUpdate := []string{} envFromSourceToRemove := []string{} for _, name := range p.EnvFrom { if name == "-" { return fmt.Errorf("\"-\" is not a valid value for \"--env-from\"") } else if strings.HasSuffix(name, "-") { envFromSourceToRemove = append(envFromSourceToRemove, name[:len(name)-1]) } else { envFromSourceToUpdate = append(envFromSourceToUpdate, name) } } err := UpdateEnvFrom(podSpec, envFromSourceToUpdate, envFromSourceToRemove) if err != nil { return err } } if flags.Changed("mount") || flags.Changed("volume") { mountsToUpdate, mountsToRemove, err := util.OrderedMapAndRemovalListFromArray(p.Mount, "=") if err != nil { return fmt.Errorf("Invalid --mount: %w", err) } volumesToUpdate, volumesToRemove, err := util.OrderedMapAndRemovalListFromArray(p.Volume, "=") if err != nil { return fmt.Errorf("Invalid --volume: %w", err) } err = UpdateVolumeMountsAndVolumes(podSpec, mountsToUpdate, mountsToRemove, volumesToUpdate, volumesToRemove) if err != nil { return err } } if flags.Changed("image") { err = UpdateImage(podSpec, p.Image.String()) if err != nil { return err } } if flags.Changed("pull-policy") { err = UpdateImagePullPolicy(podSpec, p.ImagePullPolicy) if err != nil { return err } } requestsToRemove, limitsToRemove, err := p.Resources.Validate() if err != nil { return err } err = UpdateResources(podSpec, p.Resources.ResourceRequirements, requestsToRemove, limitsToRemove) if err != nil { return err } if flags.Changed("cmd") { err = UpdateContainerCommand(podSpec, p.Command) if err != nil { return err } } if flags.Changed("arg") { err = UpdateContainerArg(podSpec, p.Arg) if err != nil { return err } } if flags.Changed("port") { err = UpdateContainerPort(podSpec, p.Port) if err != nil { return err } } if flags.Changed("service-account") { UpdateServiceAccountName(podSpec, p.ServiceAccountName) } if flags.Changed("pull-secret") { UpdateImagePullSecrets(podSpec, p.ImagePullSecrets) } if flags.Changed("containers") || flags.Changed("extra-containers") || p.ExtraContainers == "-" { var fromFile *corev1.PodSpec fromFile, err = decodeContainersFromFile(p.ExtraContainers) if err != nil { return err } UpdateContainers(podSpec, fromFile.Containers) } if flags.Changed("probe-liveness") { if err := UpdateLivenessProbe(podSpec, p.LivenessProbe); err != nil { return err } } if flags.Changed("probe-liveness-opts") { if err := UpdateLivenessProbeOpts(podSpec, p.LivenessProbeOpts); err != nil { return err } } if flags.Changed("probe-readiness") { if err := UpdateReadinessProbe(podSpec, p.ReadinessProbe); err != nil { return err } } if flags.Changed("probe-readiness-opts") { if err := UpdateReadinessProbeOpts(podSpec, p.ReadinessProbeOpts); err != nil { return err } } if flags.Changed("security-context") { if err := UpdateSecurityContext(podSpec, p.SecurityContext); err != nil { return err } } if flags.Changed("node-selector") { if err := UpdateNodeSelector(podSpec, p.NodeSelector); err != nil { return fmt.Errorf("Invalid --node-selector: %w", err) } } if flags.Changed("toleration") { err = UpdateTolerations(podSpec, p.Toleration) if err != nil { return fmt.Errorf("Invalid --toleration: %w", err) } } if flags.Changed("node-affinity") { err = UpdateNodeAffinity(podSpec, p.NodeAffinity) if err != nil { return fmt.Errorf("Invalid --node-affinity: %w", err) } } if flags.Changed("user") { err = UpdateUser(podSpec, p.User) if err != nil { return err } } return nil }