mirror of https://github.com/knative/client.git
Add support for envFrom and volumeMounts (#393)
* Add support for envFrom, volumeMounts, serviceAccountName, and imagePullPolicy * Add comments for added functions * Add test functions for --env-from * Add test functions for --volume-mount * Add test functions for --service-account-name and --image-pull-policy * Add testing functions to cover the case "service update --env-from" * Add test functions to cover the case "service update --volume-mount" * Add missing test functions for config_changes.go * Add testing functions for pkg/util/parsing_helper.go * Fix a bug on a test case, caused by ignoring random orderedness of map * Remove image-pull-policy because it is not supported by Knative * Fix comments to clarify it as well as to fix a typo * Remove service-account-name flags in order to submit it as a seperate PR * Split --volume-mount flag into --volume-mount and --volume flags in order to enable multiple times mounting for the same volume * Change the name of local variable to simplify * Update docs * Change the flag "--volume-mount" into "--mount", and fix it to make volume automatically when the config-map or secret is directley used. In addition, the test cases are changed to new unit test style. To keep the original orderedness given via flags, OrderedMap implementation is added as well * Fix usage descriptions for the mount flag * Factor out the name existence checking from createEnvFromSource * Fix the usage description for --mount * Sanitize a generated volume name
This commit is contained in:
parent
e6f15f9b58
commit
9d759ca6e4
|
|
@ -47,6 +47,7 @@ kn service create NAME --image IMAGE [flags]
|
|||
--concurrency-limit int Hard Limit of concurrent requests to be processed by a single replica.
|
||||
--concurrency-target int Recommendation for when to scale up based on the concurrent number of incoming request. Defaults to --concurrency-limit when given.
|
||||
-e, --env stringArray 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-).
|
||||
--env-from stringArray 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-.
|
||||
--force Create service forcefully, replaces existing service if any.
|
||||
-h, --help help for create
|
||||
--image string Image to run.
|
||||
|
|
@ -56,6 +57,7 @@ kn service create NAME --image IMAGE [flags]
|
|||
--lock-to-digest keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) (default true)
|
||||
--max-scale int Maximal number of replicas.
|
||||
--min-scale int Minimal number of replicas.
|
||||
--mount stringArray Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
|
||||
-n, --namespace string Specify the namespace to operate in.
|
||||
--no-lock-to-digest do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
|
||||
-p, --port int32 The port where application listens on.
|
||||
|
|
@ -63,6 +65,7 @@ kn service create NAME --image IMAGE [flags]
|
|||
--requests-memory string The requested memory (e.g., 64Mi).
|
||||
--revision-name string The revision name to set. Must start with the service name and a dash as a prefix. Empty revision name will result in the server generating a name for the revision. Accepts golang templates, allowing {{.Service}} for the service name, {{.Generation}} for the generation, and {{.Random [n]}} for n random consonants. (default "{{.Service}}-{{.Random 5}}-{{.Generation}}")
|
||||
--service-account string Service account name to set. Empty service account name will result to clear the service account.
|
||||
--volume stringArray Add a volume from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret: or sc:). Example: --volume myvolume=cm:myconfigmap or --volume myvolume=secret:mysecret. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --volume myvolume-.
|
||||
--wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 600)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ kn service update NAME [flags]
|
|||
--concurrency-limit int Hard Limit of concurrent requests to be processed by a single replica.
|
||||
--concurrency-target int Recommendation for when to scale up based on the concurrent number of incoming request. Defaults to --concurrency-limit when given.
|
||||
-e, --env stringArray 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-).
|
||||
--env-from stringArray 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-.
|
||||
-h, --help help for update
|
||||
--image string Image to run.
|
||||
-l, --label stringArray Service label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-).
|
||||
|
|
@ -51,6 +52,7 @@ kn service update NAME [flags]
|
|||
--lock-to-digest keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) (default true)
|
||||
--max-scale int Maximal number of replicas.
|
||||
--min-scale int Minimal number of replicas.
|
||||
--mount stringArray Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
|
||||
-n, --namespace string Specify the namespace to operate in.
|
||||
--no-lock-to-digest do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
|
||||
-p, --port int32 The port where application listens on.
|
||||
|
|
@ -60,7 +62,8 @@ kn service update NAME [flags]
|
|||
--service-account string Service account name to set. Empty service account name will result to clear the service account.
|
||||
--tag strings Set tag (format: --tag revisionRef=tagName) where revisionRef can be a revision or '@latest' string representing latest ready revision. This flag can be specified multiple times.
|
||||
--traffic strings Set traffic distribution (format: --traffic revisionRef=percent) where revisionRef can be a revision or a tag or '@latest' string representing latest ready revision. This flag can be given multiple times with percent summing up to 100%.
|
||||
--untag strings Untag revision (format: --untag tagName). This flag can be spcified multiple times.
|
||||
--untag strings Untag revision (format: --untag tagName). This flag can be specified multiple times.
|
||||
--volume stringArray Add a volume from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret: or sc:). Example: --volume myvolume=cm:myconfigmap or --volume myvolume=secret:mysecret. You can use this flag multiple times. To unset a ConfigMap/Secret reference, append "-" to the name, e.g. --volume myvolume-.
|
||||
--wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 600)
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func (t *Traffic) Add(cmd *cobra.Command) {
|
|||
cmd.Flags().StringSliceVar(&t.UntagRevisions,
|
||||
"untag",
|
||||
nil,
|
||||
"Untag revision (format: --untag tagName). This flag can be spcified multiple times.")
|
||||
"Untag revision (format: --untag tagName). This flag can be specified multiple times.")
|
||||
}
|
||||
|
||||
func (t *Traffic) PercentagesChanged(cmd *cobra.Command) bool {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -29,8 +30,12 @@ import (
|
|||
|
||||
type ConfigurationEditFlags struct {
|
||||
// Direct field manipulation
|
||||
Image string
|
||||
Env []string
|
||||
Image string
|
||||
Env []string
|
||||
EnvFrom []string
|
||||
Mount []string
|
||||
Volume []string
|
||||
|
||||
RequestsFlags, LimitsFlags ResourceFlags
|
||||
MinScale int
|
||||
MaxScale int
|
||||
|
|
@ -72,6 +77,29 @@ func (p *ConfigurationEditFlags) addSharedFlags(command *cobra.Command) {
|
|||
"any number of times to set multiple environment variables. "+
|
||||
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
|
||||
p.markFlagMakesRevision("env")
|
||||
|
||||
command.Flags().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-.")
|
||||
p.markFlagMakesRevision("env-from")
|
||||
|
||||
command.Flags().StringArrayVarP(&p.Mount, "mount", "", []string{},
|
||||
"Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. "+
|
||||
"Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. "+
|
||||
"When a configmap or a secret is specified, a corresponding volume is automatically generated. "+
|
||||
"You can use this flag multiple times. "+
|
||||
"For unmounting a directory, append \"-\", e.g. --mount /mydir-, which also removes any auto-generated volume.")
|
||||
p.markFlagMakesRevision("mount")
|
||||
|
||||
command.Flags().StringArrayVarP(&p.Volume, "volume", "", []string{},
|
||||
"Add a volume from a ConfigMap (prefix cm: or config-map:) or a Secret (prefix secret: or sc:). "+
|
||||
"Example: --volume myvolume=cm:myconfigmap or --volume myvolume=secret:mysecret. "+
|
||||
"You can use this flag multiple times. "+
|
||||
"To unset a ConfigMap/Secret reference, append \"-\" to the name, e.g. --volume myvolume-.")
|
||||
p.markFlagMakesRevision("volume")
|
||||
|
||||
command.Flags().StringVar(&p.RequestsFlags.CPU, "requests-cpu", "", "The requested CPU (e.g., 250m).")
|
||||
p.markFlagMakesRevision("requests-cpu")
|
||||
command.Flags().StringVar(&p.RequestsFlags.Memory, "requests-memory", "", "The requested memory (e.g., 64Mi).")
|
||||
|
|
@ -105,6 +133,7 @@ func (p *ConfigurationEditFlags) addSharedFlags(command *cobra.Command) {
|
|||
"Accepts golang templates, allowing {{.Service}} for the service name, "+
|
||||
"{{.Generation}} for the generation, and {{.Random [n]}} for n random consonants.")
|
||||
p.markFlagMakesRevision("revision-name")
|
||||
|
||||
flags.AddBothBoolFlagsUnhidden(command.Flags(), &p.LockToDigest, "lock-to-digest", "", true,
|
||||
"keep the running image for the service constant when not explicitly specifying "+
|
||||
"the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)")
|
||||
|
|
@ -160,6 +189,42 @@ func (p *ConfigurationEditFlags) Apply(
|
|||
}
|
||||
}
|
||||
|
||||
if cmd.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 = servinglib.UpdateEnvFrom(template, envFromSourceToUpdate, envFromSourceToRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Flags().Changed("mount") || cmd.Flags().Changed("volume") {
|
||||
mountsToUpdate, mountsToRemove, err := util.OrderedMapAndRemovalListFromArray(p.Mount, "=")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid --mount")
|
||||
}
|
||||
|
||||
volumesToUpdate, volumesToRemove, err := util.OrderedMapAndRemovalListFromArray(p.Volume, "=")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Invalid --volume")
|
||||
}
|
||||
|
||||
err = servinglib.UpdateVolumeMountsAndVolumes(template, mountsToUpdate, mountsToRemove, volumesToUpdate, volumesToRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
name, err := servinglib.GenerateRevisionName(p.RevisionName, service)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -113,6 +113,288 @@ func TestServiceCreateLabel(t *testing.T) {
|
|||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithEnvFromConfigMap(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "config-map-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "config-map:config-map-name", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithEnvFromConfigMapRemoval(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.GetContainer().EnvFrom = nil
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "config-map:config-map-name-", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithEnvFromEmptyRemoval(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.GetContainer().EnvFrom = nil
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
_, err = executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "-", "--async", "--revision-name=")
|
||||
assert.Error(t, err, "\"-\" is not a valid value for \"--env-from\"")
|
||||
}
|
||||
|
||||
func TestServiceCreateWithEnvFromSecret(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{
|
||||
{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "secret-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "secret:secret-name", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithEnvFromSecretRemoval(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.GetContainer().EnvFrom = nil
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "secret:secret-name-", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithVolumeAndMountConfigMap(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "volume-name",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "config-map-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "volume-name",
|
||||
MountPath: "/mount/path",
|
||||
ReadOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
|
||||
"--mount", "/mount/path=volume-name", "--volume", "volume-name=cm:config-map-name", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithMountConfigMap(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: servinglib.GenerateVolumeName("/mount/path"),
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "config-map-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: servinglib.GenerateVolumeName("/mount/path"),
|
||||
MountPath: "/mount/path",
|
||||
ReadOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
|
||||
"--mount", "/mount/path=cm:config-map-name", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithVolumeAndMountSecret(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: "volume-name",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: "secret-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: "volume-name",
|
||||
MountPath: "/mount/path",
|
||||
ReadOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
|
||||
"--mount", "/mount/path=volume-name", "--volume", "volume-name=secret:secret-name", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func TestServiceCreateWithMountSecret(t *testing.T) {
|
||||
client := knclient.NewMockKnClient(t)
|
||||
|
||||
r := client.Recorder()
|
||||
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
|
||||
|
||||
service := getService("foo")
|
||||
template, err := servinglib.RevisionTemplateOfService(service)
|
||||
assert.NilError(t, err)
|
||||
template.Spec.Volumes = []corev1.Volume{
|
||||
{
|
||||
Name: servinglib.GenerateVolumeName("/mount/path"),
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: "secret-name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
|
||||
{
|
||||
Name: servinglib.GenerateVolumeName("/mount/path"),
|
||||
MountPath: "/mount/path",
|
||||
ReadOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
|
||||
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
|
||||
r.CreateService(service, nil)
|
||||
|
||||
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
|
||||
"--mount", "/mount/path=sc:secret-name", "--async", "--revision-name=")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
|
||||
|
||||
r.Validate()
|
||||
}
|
||||
|
||||
func getService(name string) *v1alpha1.Service {
|
||||
service := &v1alpha1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,20 +15,40 @@
|
|||
package serving
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"knative.dev/client/pkg/util"
|
||||
"knative.dev/pkg/ptr"
|
||||
"knative.dev/serving/pkg/apis/autoscaling"
|
||||
"knative.dev/serving/pkg/apis/serving"
|
||||
servingv1alpha1 "knative.dev/serving/pkg/apis/serving/v1alpha1"
|
||||
)
|
||||
|
||||
// VolumeSourceType is a type standing for enumeration of ConfigMap and Secret
|
||||
type VolumeSourceType int
|
||||
|
||||
// Enumeration of volume source types: ConfigMap or Secret
|
||||
const (
|
||||
ConfigMapVolumeSourceType VolumeSourceType = iota
|
||||
SecretVolumeSourceType
|
||||
)
|
||||
|
||||
func (vt VolumeSourceType) String() string {
|
||||
names := [...]string{"config-map", "secret"}
|
||||
if vt < ConfigMapVolumeSourceType || vt > SecretVolumeSourceType {
|
||||
return "unknown"
|
||||
}
|
||||
return names[vt]
|
||||
}
|
||||
|
||||
var UserImageAnnotationKey = "client.knative.dev/user-image"
|
||||
|
||||
// UpdateEnvVars gives the configuration all the env var values listed in the given map of
|
||||
|
|
@ -50,12 +70,115 @@ func UpdateEnvVars(template *servingv1alpha1.RevisionTemplateSpec, toUpdate map[
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateEnvFrom updates envFrom
|
||||
func UpdateEnvFrom(template *servingv1alpha1.RevisionTemplateSpec, toUpdate []string, toRemove []string) error {
|
||||
container, err := ContainerOfRevisionTemplate(template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
envFrom, err := updateEnvFrom(container.EnvFrom, toUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
container.EnvFrom, err = removeEnvFrom(envFrom, toRemove)
|
||||
return err
|
||||
}
|
||||
|
||||
func reviseVolumeInfoAndMountsToUpdate(volumes []corev1.Volume, mountsToUpdate *util.OrderedMap,
|
||||
volumesToUpdate *util.OrderedMap) (*util.OrderedMap, *util.OrderedMap, error) {
|
||||
volumeSourceInfoByName := util.NewOrderedMap() //make(map[string]*volumeSourceInfo)
|
||||
mountsToUpdateRevised := util.NewOrderedMap() //make(map[string]string)
|
||||
|
||||
it := mountsToUpdate.Iterator()
|
||||
for path, value, ok := it.NextString(); ok; path, value, ok = it.NextString() {
|
||||
// slices[0] -> config-map, cm, secret, sc, volume, or vo
|
||||
// slices[1] -> secret, config-map, or volume name
|
||||
slices := strings.SplitN(value, ":", 2)
|
||||
if len(slices) == 1 {
|
||||
mountsToUpdateRevised.Set(path, slices[0])
|
||||
} else {
|
||||
switch volumeType := slices[0]; volumeType {
|
||||
case "config-map", "cm":
|
||||
generatedName := GenerateVolumeName(path)
|
||||
volumeSourceInfoByName.Set(generatedName, &volumeSourceInfo{
|
||||
volumeSourceType: ConfigMapVolumeSourceType,
|
||||
volumeSourceName: slices[1],
|
||||
})
|
||||
mountsToUpdateRevised.Set(path, generatedName)
|
||||
case "secret", "sc":
|
||||
generatedName := GenerateVolumeName(path)
|
||||
volumeSourceInfoByName.Set(generatedName, &volumeSourceInfo{
|
||||
volumeSourceType: SecretVolumeSourceType,
|
||||
volumeSourceName: slices[1],
|
||||
})
|
||||
mountsToUpdateRevised.Set(path, generatedName)
|
||||
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unsupported volume type \"%q\"; supported volume types are \"config-map or cm\", \"secret or sc\", and \"volume or vo\"", slices[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it = volumesToUpdate.Iterator()
|
||||
for name, value, ok := it.NextString(); ok; name, value, ok = it.NextString() {
|
||||
info, err := newVolumeSourceInfoWithSpecString(value)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
volumeSourceInfoByName.Set(name, info)
|
||||
}
|
||||
|
||||
return volumeSourceInfoByName, mountsToUpdateRevised, nil
|
||||
}
|
||||
|
||||
func reviseVolumesToRemove(volumeMounts []corev1.VolumeMount, volumesToRemove []string, mountsToRemove []string) []string {
|
||||
for _, pathToRemove := range mountsToRemove {
|
||||
for _, volumeMount := range volumeMounts {
|
||||
if volumeMount.MountPath == pathToRemove && volumeMount.Name == GenerateVolumeName(pathToRemove) {
|
||||
volumesToRemove = append(volumesToRemove, volumeMount.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return volumesToRemove
|
||||
}
|
||||
|
||||
// UpdateVolumeMountsAndVolumes updates the configuration for volume mounts and volumes.
|
||||
func UpdateVolumeMountsAndVolumes(template *servingv1alpha1.RevisionTemplateSpec,
|
||||
mountsToUpdate *util.OrderedMap, mountsToRemove []string, volumesToUpdate *util.OrderedMap, volumesToRemove []string) error {
|
||||
container, err := ContainerOfRevisionTemplate(template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumeSourceInfoByName, mountsToUpdate, err := reviseVolumeInfoAndMountsToUpdate(template.Spec.Volumes, mountsToUpdate, volumesToUpdate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes, err := updateVolumesFromMap(template.Spec.Volumes, volumeSourceInfoByName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumeMounts, err := updateVolumeMountsFromMap(container.VolumeMounts, mountsToUpdate, volumes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumesToRemove = reviseVolumesToRemove(container.VolumeMounts, volumesToRemove, mountsToRemove)
|
||||
|
||||
container.VolumeMounts = removeVolumeMounts(volumeMounts, mountsToRemove)
|
||||
template.Spec.Volumes, err = removeVolumes(volumes, volumesToRemove, container.VolumeMounts)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateMinScale updates min scale annotation
|
||||
func UpdateMinScale(template *servingv1alpha1.RevisionTemplateSpec, min int) error {
|
||||
return UpdateRevisionTemplateAnnotation(template, autoscaling.MinScaleAnnotationKey, strconv.Itoa(min))
|
||||
}
|
||||
|
||||
// UpdatMaxScale updates max scale annotation
|
||||
// UpdateMaxScale updates max scale annotation
|
||||
func UpdateMaxScale(template *servingv1alpha1.RevisionTemplateSpec, max int) error {
|
||||
return UpdateRevisionTemplateAnnotation(template, autoscaling.MaxScaleAnnotationKey, strconv.Itoa(max))
|
||||
}
|
||||
|
|
@ -172,7 +295,7 @@ func FreezeImageToDigest(template *servingv1alpha1.RevisionTemplateSpec, baseRev
|
|||
return err
|
||||
}
|
||||
if currentContainer.Image != baseContainer.Image {
|
||||
return fmt.Errorf("could not freeze image to digest since current revision contains unexpected image.")
|
||||
return fmt.Errorf("could not freeze image to digest since current revision contains unexpected image")
|
||||
}
|
||||
|
||||
if baseRevision.Status.ImageDigest != "" {
|
||||
|
|
@ -274,6 +397,29 @@ func UpdateServiceAccountName(template *servingv1alpha1.RevisionTemplateSpec, se
|
|||
return nil
|
||||
}
|
||||
|
||||
// GenerateVolumeName generates a volume name with respect to a given path string.
|
||||
// Current implementation basically sanitizes the path string by changing "/" into "."
|
||||
// To reduce any chance of duplication, a checksum part generated from the path string is appended to the sanitized string.
|
||||
func GenerateVolumeName(path string) string {
|
||||
builder := &strings.Builder{}
|
||||
for idx, r := range path {
|
||||
switch {
|
||||
case unicode.IsLower(r) || unicode.IsDigit(r) || r == '-' || r == '.':
|
||||
builder.WriteRune(r)
|
||||
case unicode.IsUpper(r):
|
||||
builder.WriteRune(unicode.ToLower(r))
|
||||
case r == '/':
|
||||
if idx != 0 {
|
||||
builder.WriteRune('.')
|
||||
}
|
||||
default:
|
||||
builder.WriteRune('-')
|
||||
}
|
||||
}
|
||||
|
||||
return appendCheckSum(builder.String(), path)
|
||||
}
|
||||
|
||||
// =======================================================================================
|
||||
|
||||
func updateEnvVarsFromMap(env []corev1.EnvVar, toUpdate map[string]string) []corev1.EnvVar {
|
||||
|
|
@ -304,3 +450,257 @@ func removeEnvVars(env []corev1.EnvVar, toRemove []string) []corev1.EnvVar {
|
|||
}
|
||||
return env
|
||||
}
|
||||
|
||||
func updateEnvFrom(envFromSources []corev1.EnvFromSource, toUpdate []string) ([]corev1.EnvFromSource, error) {
|
||||
existingNameSet := make(map[string]bool)
|
||||
|
||||
for _, envSrc := range envFromSources {
|
||||
if canonicalName, err := getCanonicalNameFromEnvFromSource(&envSrc); err == nil {
|
||||
existingNameSet[canonicalName] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range toUpdate {
|
||||
info, err := newVolumeSourceInfoWithSpecString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := existingNameSet[info.getCanonicalName()]; !ok {
|
||||
envFromSources = append(envFromSources, *info.createEnvFromSource())
|
||||
}
|
||||
}
|
||||
|
||||
return envFromSources, nil
|
||||
}
|
||||
|
||||
func removeEnvFrom(envFromSources []corev1.EnvFromSource, toRemove []string) ([]corev1.EnvFromSource, error) {
|
||||
for _, name := range toRemove {
|
||||
info, err := newVolumeSourceInfoWithSpecString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, envSrc := range envFromSources {
|
||||
if (info.volumeSourceType == ConfigMapVolumeSourceType && envSrc.ConfigMapRef != nil && info.volumeSourceName == envSrc.ConfigMapRef.Name) ||
|
||||
(info.volumeSourceType == SecretVolumeSourceType && envSrc.SecretRef != nil && info.volumeSourceName == envSrc.SecretRef.Name) {
|
||||
envFromSources = append(envFromSources[:i], envFromSources[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(envFromSources) == 0 {
|
||||
envFromSources = nil
|
||||
}
|
||||
|
||||
return envFromSources, nil
|
||||
}
|
||||
|
||||
func updateVolume(volume *corev1.Volume, info *volumeSourceInfo) error {
|
||||
switch info.volumeSourceType {
|
||||
case ConfigMapVolumeSourceType:
|
||||
volume.Secret = nil
|
||||
volume.ConfigMap = &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: info.volumeSourceName}}
|
||||
case SecretVolumeSourceType:
|
||||
volume.ConfigMap = nil
|
||||
volume.Secret = &corev1.SecretVolumeSource{SecretName: info.volumeSourceName}
|
||||
default:
|
||||
return fmt.Errorf("Invalid VolumeSourceType")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateVolumeMountsFromMap updates or adds volume mounts. If a given name of a volume is not existing, it returns an error
|
||||
func updateVolumeMountsFromMap(volumeMounts []corev1.VolumeMount, toUpdate *util.OrderedMap, volumes []corev1.Volume) ([]corev1.VolumeMount, error) {
|
||||
set := make(map[string]bool)
|
||||
|
||||
for i := range volumeMounts {
|
||||
volumeMount := &volumeMounts[i]
|
||||
name, present := toUpdate.GetString(volumeMount.MountPath)
|
||||
|
||||
if present {
|
||||
if !existsVolumeNameInVolumes(name, volumes) {
|
||||
return nil, fmt.Errorf("There is no volume matched with %q", name)
|
||||
}
|
||||
|
||||
volumeMount.ReadOnly = true
|
||||
volumeMount.Name = name
|
||||
set[volumeMount.MountPath] = true
|
||||
}
|
||||
}
|
||||
|
||||
it := toUpdate.Iterator()
|
||||
for mountPath, name, ok := it.NextString(); ok; mountPath, name, ok = it.NextString() {
|
||||
if !set[mountPath] {
|
||||
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
||||
Name: name,
|
||||
ReadOnly: true,
|
||||
MountPath: mountPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return volumeMounts, nil
|
||||
}
|
||||
|
||||
func removeVolumeMounts(volumeMounts []corev1.VolumeMount, toRemove []string) []corev1.VolumeMount {
|
||||
for _, mountPath := range toRemove {
|
||||
for i, volumeMount := range volumeMounts {
|
||||
if volumeMount.MountPath == mountPath {
|
||||
volumeMounts = append(volumeMounts[:i], volumeMounts[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(volumeMounts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return volumeMounts
|
||||
}
|
||||
|
||||
// updateVolumesFromMap updates or adds volumes regardless whether the volume is used or not
|
||||
func updateVolumesFromMap(volumes []corev1.Volume, toUpdate *util.OrderedMap) ([]corev1.Volume, error) {
|
||||
set := make(map[string]bool)
|
||||
|
||||
for i := range volumes {
|
||||
volume := &volumes[i]
|
||||
info, present := toUpdate.Get(volume.Name)
|
||||
if present {
|
||||
err := updateVolume(volume, info.(*volumeSourceInfo))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
set[volume.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
it := toUpdate.Iterator()
|
||||
for name, info, ok := it.Next(); ok; name, info, ok = it.Next() {
|
||||
if !set[name] {
|
||||
volumes = append(volumes, corev1.Volume{Name: name})
|
||||
updateVolume(&volumes[len(volumes)-1], info.(*volumeSourceInfo))
|
||||
}
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// removeVolumes removes volumes. If there is a volume mount referencing the volume, it causes an error
|
||||
func removeVolumes(volumes []corev1.Volume, toRemove []string, volumeMounts []corev1.VolumeMount) ([]corev1.Volume, error) {
|
||||
for _, name := range toRemove {
|
||||
for i, volume := range volumes {
|
||||
if volume.Name == name {
|
||||
if existsVolumeNameInVolumeMounts(name, volumeMounts) {
|
||||
return nil, fmt.Errorf("The volume %q cannot be removed because it is mounted", name)
|
||||
}
|
||||
volumes = append(volumes[:i], volumes[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(volumes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// =======================================================================================
|
||||
|
||||
type volumeSourceInfo struct {
|
||||
volumeSourceType VolumeSourceType
|
||||
volumeSourceName string
|
||||
}
|
||||
|
||||
func newVolumeSourceInfoWithSpecString(spec string) (*volumeSourceInfo, error) {
|
||||
slices := strings.SplitN(spec, ":", 2)
|
||||
if len(slices) != 2 {
|
||||
return nil, fmt.Errorf("argument requires a value that contains the : character; got %q", spec)
|
||||
}
|
||||
|
||||
var volumeSourceType VolumeSourceType
|
||||
|
||||
typeString := strings.TrimSpace(slices[0])
|
||||
volumeSourceName := strings.TrimSpace(slices[1])
|
||||
|
||||
switch typeString {
|
||||
case "config-map", "cm":
|
||||
volumeSourceType = ConfigMapVolumeSourceType
|
||||
case "secret", "sc":
|
||||
volumeSourceType = SecretVolumeSourceType
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported volume source type \"%q\"; supported volume source types are \"config-map\" and \"secret\"", slices[0])
|
||||
}
|
||||
|
||||
if len(volumeSourceName) == 0 {
|
||||
return nil, fmt.Errorf("the name of %s cannot be an empty string", volumeSourceType)
|
||||
}
|
||||
|
||||
return &volumeSourceInfo{
|
||||
volumeSourceType: volumeSourceType,
|
||||
volumeSourceName: volumeSourceName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (vol *volumeSourceInfo) getCanonicalName() string {
|
||||
return fmt.Sprintf("%s:%s", vol.volumeSourceType, vol.volumeSourceName)
|
||||
}
|
||||
|
||||
func getCanonicalNameFromEnvFromSource(envSrc *corev1.EnvFromSource) (string, error) {
|
||||
if envSrc.ConfigMapRef != nil {
|
||||
return fmt.Sprintf("%s:%s", ConfigMapVolumeSourceType, envSrc.ConfigMapRef.Name), nil
|
||||
}
|
||||
if envSrc.SecretRef != nil {
|
||||
return fmt.Sprintf("%s:%s", SecretVolumeSourceType, envSrc.SecretRef.Name), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("there is no ConfigMapRef or SecretRef in a EnvFromSource")
|
||||
}
|
||||
|
||||
func (vol *volumeSourceInfo) createEnvFromSource() *corev1.EnvFromSource {
|
||||
switch vol.volumeSourceType {
|
||||
case ConfigMapVolumeSourceType:
|
||||
return &corev1.EnvFromSource{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: vol.volumeSourceName,
|
||||
}}}
|
||||
case SecretVolumeSourceType:
|
||||
return &corev1.EnvFromSource{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: vol.volumeSourceName,
|
||||
}}}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =======================================================================================
|
||||
|
||||
func existsVolumeNameInVolumes(volumeName string, volumes []corev1.Volume) bool {
|
||||
for _, volume := range volumes {
|
||||
if volume.Name == volumeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func existsVolumeNameInVolumeMounts(volumeName string, volumeMounts []corev1.VolumeMount) bool {
|
||||
for _, volumeMount := range volumeMounts {
|
||||
if volumeMount.Name == volumeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appendCheckSum(sanitiedString string, path string) string {
|
||||
checkSum := sha1.Sum([]byte(path))
|
||||
shortCheckSum := checkSum[0:4]
|
||||
return fmt.Sprintf("%s-%x", sanitiedString, shortCheckSum)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
"gotest.tools/assert"
|
||||
|
||||
"knative.dev/client/pkg/util"
|
||||
"knative.dev/pkg/ptr"
|
||||
"knative.dev/serving/pkg/apis/autoscaling"
|
||||
|
||||
|
|
@ -409,6 +410,136 @@ func TestUpdateLabelsRemoveExisting(t *testing.T) {
|
|||
assert.DeepEqual(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestUpdateEnvFrom(t *testing.T) {
|
||||
template, container := getV1alpha1RevisionTemplateWithOldFields()
|
||||
container.EnvFrom = append(container.EnvFrom,
|
||||
corev1.EnvFromSource{
|
||||
ConfigMapRef: &corev1.ConfigMapEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "config-map-existing-name",
|
||||
}}},
|
||||
corev1.EnvFromSource{
|
||||
SecretRef: &corev1.SecretEnvSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "secret-existing-name",
|
||||
}}},
|
||||
)
|
||||
UpdateEnvFrom(template,
|
||||
[]string{"config-map:config-map-new-name-1", "secret:secret-new-name-1"},
|
||||
[]string{"config-map:config-map-existing-name", "secret:secret-existing-name"})
|
||||
assert.Equal(t, len(container.EnvFrom), 2)
|
||||
assert.Equal(t, container.EnvFrom[0].ConfigMapRef.Name, "config-map-new-name-1")
|
||||
assert.Equal(t, container.EnvFrom[1].SecretRef.Name, "secret-new-name-1")
|
||||
}
|
||||
|
||||
func TestUpdateVolumeMountsAndVolumes(t *testing.T) {
|
||||
template, container := getV1alpha1RevisionTemplateWithOldFields()
|
||||
template.Spec.Volumes = append(template.Spec.Volumes,
|
||||
corev1.Volume{
|
||||
Name: "existing-config-map-volume-name-1",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "existing-config-map-1",
|
||||
}}}},
|
||||
corev1.Volume{
|
||||
Name: "existing-config-map-volume-name-2",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: "existing-config-map-2",
|
||||
}}}},
|
||||
corev1.Volume{
|
||||
Name: "existing-secret-volume-name-1",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: "existing-secret-1",
|
||||
}}},
|
||||
corev1.Volume{
|
||||
Name: "existing-secret-volume-name-2",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: "existing-secret-2",
|
||||
}}})
|
||||
|
||||
container.VolumeMounts = append(container.VolumeMounts,
|
||||
corev1.VolumeMount{
|
||||
Name: "existing-config-map-volume-name-1",
|
||||
ReadOnly: true,
|
||||
MountPath: "/existing-config-map-1/mount/path",
|
||||
},
|
||||
corev1.VolumeMount{
|
||||
Name: "existing-config-map-volume-name-2",
|
||||
ReadOnly: true,
|
||||
MountPath: "/existing-config-map-2/mount/path",
|
||||
},
|
||||
corev1.VolumeMount{
|
||||
Name: "existing-secret-volume-name-1",
|
||||
ReadOnly: true,
|
||||
MountPath: "/existing-secret-1/mount/path",
|
||||
},
|
||||
corev1.VolumeMount{
|
||||
Name: "existing-secret-volume-name-2",
|
||||
ReadOnly: true,
|
||||
MountPath: "/existing-secret-2/mount/path",
|
||||
},
|
||||
)
|
||||
|
||||
err := UpdateVolumeMountsAndVolumes(template,
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"/new-config-map/mount/path", "new-config-map-volume-name"}}),
|
||||
[]string{},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"new-config-map-volume-name", "config-map:new-config-map"}}),
|
||||
[]string{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = UpdateVolumeMountsAndVolumes(template,
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"/updated-config-map/mount/path", "existing-config-map-volume-name-2"}}),
|
||||
[]string{},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"existing-config-map-volume-name-2", "config-map:updated-config-map"}}),
|
||||
[]string{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = UpdateVolumeMountsAndVolumes(template,
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"/new-secret/mount/path", "new-secret-volume-name"}}),
|
||||
[]string{},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"new-secret-volume-name", "secret:new-secret"}}),
|
||||
[]string{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = UpdateVolumeMountsAndVolumes(template,
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"/updated-secret/mount/path", "existing-secret-volume-name-2"}}),
|
||||
[]string{"/existing-config-map-1/mount/path",
|
||||
"/existing-secret-1/mount/path"},
|
||||
util.NewOrderedMapWithKVStrings([][]string{{"existing-secret-volume-name-2", "secret:updated-secret"}}),
|
||||
[]string{"existing-config-map-volume-name-1",
|
||||
"existing-secret-volume-name-1"})
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, len(template.Spec.Volumes), 4)
|
||||
assert.Equal(t, len(container.VolumeMounts), 6)
|
||||
assert.Equal(t, template.Spec.Volumes[0].Name, "existing-config-map-volume-name-2")
|
||||
assert.Equal(t, template.Spec.Volumes[0].ConfigMap.Name, "updated-config-map")
|
||||
assert.Equal(t, template.Spec.Volumes[1].Name, "existing-secret-volume-name-2")
|
||||
assert.Equal(t, template.Spec.Volumes[1].Secret.SecretName, "updated-secret")
|
||||
assert.Equal(t, template.Spec.Volumes[2].Name, "new-config-map-volume-name")
|
||||
assert.Equal(t, template.Spec.Volumes[2].ConfigMap.Name, "new-config-map")
|
||||
assert.Equal(t, template.Spec.Volumes[3].Name, "new-secret-volume-name")
|
||||
assert.Equal(t, template.Spec.Volumes[3].Secret.SecretName, "new-secret")
|
||||
|
||||
assert.Equal(t, container.VolumeMounts[0].Name, "existing-config-map-volume-name-2")
|
||||
assert.Equal(t, container.VolumeMounts[0].MountPath, "/existing-config-map-2/mount/path")
|
||||
assert.Equal(t, container.VolumeMounts[1].Name, "existing-secret-volume-name-2")
|
||||
assert.Equal(t, container.VolumeMounts[1].MountPath, "/existing-secret-2/mount/path")
|
||||
assert.Equal(t, container.VolumeMounts[2].Name, "new-config-map-volume-name")
|
||||
assert.Equal(t, container.VolumeMounts[2].MountPath, "/new-config-map/mount/path")
|
||||
assert.Equal(t, container.VolumeMounts[3].Name, "existing-config-map-volume-name-2")
|
||||
assert.Equal(t, container.VolumeMounts[3].MountPath, "/updated-config-map/mount/path")
|
||||
assert.Equal(t, container.VolumeMounts[4].Name, "new-secret-volume-name")
|
||||
assert.Equal(t, container.VolumeMounts[4].MountPath, "/new-secret/mount/path")
|
||||
assert.Equal(t, container.VolumeMounts[5].Name, "existing-secret-volume-name-2")
|
||||
assert.Equal(t, container.VolumeMounts[5].MountPath, "/updated-secret/mount/path")
|
||||
}
|
||||
|
||||
func TestUpdateServiceAccountName(t *testing.T) {
|
||||
template, _ := getV1alpha1RevisionTemplateWithOldFields()
|
||||
template.Spec.ServiceAccountName = ""
|
||||
|
|
@ -486,6 +617,28 @@ func TestUpdateAnnotationsRemoveExisting(t *testing.T) {
|
|||
assert.DeepEqual(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestGenerateVolumeName(t *testing.T) {
|
||||
actual := []string{
|
||||
"Ab12~`!@#$%^&*()-=_+[]{}|/\\<>,./?:;\"'xZ",
|
||||
"/Ab12~`!@#$%^&*()-=_+[]{}|/\\<>,./?:;\"'xZ/",
|
||||
"",
|
||||
"/",
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"ab12---------------------.----..-----xz",
|
||||
"ab12---------------------.----..-----xz.",
|
||||
"",
|
||||
"",
|
||||
}
|
||||
|
||||
for i := range actual {
|
||||
actualName := GenerateVolumeName(actual[i])
|
||||
expectedName := appendCheckSum(expected[i], actual[i])
|
||||
assert.Equal(t, actualName, expectedName)
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// =========================================================================================================
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
// Copyright © 2019 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 util
|
||||
|
||||
type valueEntry struct {
|
||||
Index int
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type orderedMapIterator struct {
|
||||
orderedMap *OrderedMap
|
||||
nextIndex int
|
||||
}
|
||||
|
||||
// OrderedMap is similar implementation of OrderedDict in Python.
|
||||
type OrderedMap struct {
|
||||
Keys []string
|
||||
ValueMap map[string]*valueEntry
|
||||
}
|
||||
|
||||
// NewOrderedMap returns new empty ordered map
|
||||
func NewOrderedMap() *OrderedMap {
|
||||
return &OrderedMap{
|
||||
Keys: []string{},
|
||||
ValueMap: map[string]*valueEntry{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewOrderedMapWithKVStrings returns new empty ordered map
|
||||
func NewOrderedMapWithKVStrings(kvList [][]string) *OrderedMap {
|
||||
o := &OrderedMap{
|
||||
Keys: []string{},
|
||||
ValueMap: map[string]*valueEntry{},
|
||||
}
|
||||
|
||||
for _, pair := range kvList {
|
||||
if len(pair) != 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o.Set(pair[0], pair[1])
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// Get returns a value corresponding the key
|
||||
func (o *OrderedMap) Get(key string) (interface{}, bool) {
|
||||
ve, ok := o.ValueMap[key]
|
||||
if ve != nil {
|
||||
return ve.Value, ok
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// GetString returns a string value corresponding the key
|
||||
func (o *OrderedMap) GetString(key string) (string, bool) {
|
||||
ve, ok := o.ValueMap[key]
|
||||
|
||||
if ve != nil {
|
||||
return ve.Value.(string), ok
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// GetStringWithDefault returns a string value corresponding the key if the key is existing.
|
||||
// Otherwise, the default value is returned.
|
||||
func (o *OrderedMap) GetStringWithDefault(key string, defaultValue string) string {
|
||||
if ve, ok := o.ValueMap[key]; ok {
|
||||
return ve.Value.(string)
|
||||
} else {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// Set append the key and value if the key is not existing on the map
|
||||
// Otherwise, the value does just replace the old value corresponding to the key.
|
||||
func (o *OrderedMap) Set(key string, value interface{}) {
|
||||
if ve, ok := o.ValueMap[key]; !ok {
|
||||
o.Keys = append(o.Keys, key)
|
||||
o.ValueMap[key] = &valueEntry{
|
||||
Index: len(o.Keys) - 1,
|
||||
Value: value,
|
||||
}
|
||||
} else {
|
||||
ve.Value = value
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes the key and value from the map
|
||||
func (o *OrderedMap) Delete(key string) {
|
||||
if ve, ok := o.ValueMap[key]; ok {
|
||||
delete(o.ValueMap, key)
|
||||
o.Keys = append(o.Keys[:ve.Index], o.Keys[ve.Index+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns a size of the ordered map
|
||||
func (o *OrderedMap) Len() int {
|
||||
return len(o.Keys)
|
||||
}
|
||||
|
||||
// Iterator creates a iterator object
|
||||
func (o *OrderedMap) Iterator() *orderedMapIterator {
|
||||
return &orderedMapIterator{
|
||||
orderedMap: o,
|
||||
nextIndex: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns key and values on current iterating cursor.
|
||||
// If the cursor moved over last entry, then the third return value will be false, otherwise true.
|
||||
func (it *orderedMapIterator) Next() (string, interface{}, bool) {
|
||||
if it.nextIndex >= it.orderedMap.Len() {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
key := it.orderedMap.Keys[it.nextIndex]
|
||||
ve, _ := it.orderedMap.ValueMap[key]
|
||||
|
||||
it.nextIndex++
|
||||
|
||||
return key, ve.Value, true
|
||||
}
|
||||
|
||||
// NextString is the same with Next, but the value is returned as string
|
||||
func (it *orderedMapIterator) NextString() (string, string, bool) {
|
||||
key, value, isValid := it.Next()
|
||||
if isValid {
|
||||
return key, value.(string), isValid
|
||||
} else {
|
||||
return "", "", isValid
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright © 2019 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 util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestOrderedMapCreate(t *testing.T) {
|
||||
initial := [][]string{{"1", "v1"}, {"2", "v2"}, {"3", "v3"}}
|
||||
o := NewOrderedMapWithKVStrings(initial)
|
||||
it := o.Iterator()
|
||||
|
||||
assert.Equal(t, o.Len(), len(initial))
|
||||
i := 0
|
||||
|
||||
for k, v, ok := it.NextString(); ok; k, v, ok = it.NextString() {
|
||||
assert.Equal(t, k, initial[i][0])
|
||||
assert.Equal(t, v, initial[i][1])
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedMapSet(t *testing.T) {
|
||||
initial := [][]string{{"1", "v1"}, {"2", "v2"}, {"3", "v3"}}
|
||||
o := NewOrderedMapWithKVStrings(initial)
|
||||
o.Set("4", "v4")
|
||||
o.Set("2", "v2-1")
|
||||
|
||||
expected := [][]string{{"1", "v1"}, {"2", "v2-1"}, {"3", "v3"}, {"4", "v4"}}
|
||||
assert.Equal(t, o.Len(), len(expected))
|
||||
|
||||
i := 0
|
||||
it := o.Iterator()
|
||||
|
||||
for k, v, ok := it.NextString(); ok; k, v, ok = it.NextString() {
|
||||
assert.Equal(t, k, expected[i][0])
|
||||
assert.Equal(t, v, expected[i][1])
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedMapGet(t *testing.T) {
|
||||
initial := [][]string{{"1", "v1"}, {"2", "v2"}, {"3", "v3"}}
|
||||
o := NewOrderedMapWithKVStrings(initial)
|
||||
o.Set("4", "v4")
|
||||
o.Set("2", "v2-1")
|
||||
|
||||
expected := [][]string{{"1", "v1"}, {"2", "v2-1"}, {"3", "v3"}, {"4", "v4"}}
|
||||
assert.Equal(t, o.Len(), len(expected))
|
||||
|
||||
for i := 0; i < len(expected); i++ {
|
||||
assert.Equal(t, o.GetStringWithDefault(expected[i][0], ""), expected[i][1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedMapDelete(t *testing.T) {
|
||||
initial := [][]string{{"1", "v1"}, {"2", "v2"}, {"3", "v3"}}
|
||||
o := NewOrderedMapWithKVStrings(initial)
|
||||
o.Set("4", "v4")
|
||||
o.Set("2", "v2-1")
|
||||
o.Delete("3")
|
||||
o.Delete("1")
|
||||
|
||||
expected := [][]string{{"2", "v2-1"}, {"4", "v4"}}
|
||||
assert.Equal(t, o.Len(), len(expected))
|
||||
|
||||
i := 0
|
||||
it := o.Iterator()
|
||||
|
||||
for k, v, ok := it.NextString(); ok; k, v, ok = it.NextString() {
|
||||
assert.Equal(t, k, expected[i][0])
|
||||
assert.Equal(t, v, expected[i][1])
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,29 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// OrderedMapAndRemovalListFromArray creates a list of key-value pair using MapFromArrayAllowingSingles, and a list of removal entries
|
||||
func OrderedMapAndRemovalListFromArray(arr []string, delimiter string) (*OrderedMap, []string, error) {
|
||||
orderedMap := NewOrderedMap()
|
||||
removalList := []string{}
|
||||
|
||||
for _, pairStr := range arr {
|
||||
pairSlice := strings.SplitN(pairStr, delimiter, 2)
|
||||
if len(pairSlice) == 0 || (len(pairSlice) == 1 && !strings.HasSuffix(pairSlice[0], "-")) {
|
||||
return nil, nil, fmt.Errorf("argument requires a value that contains the %q character; got %q", delimiter, pairStr)
|
||||
}
|
||||
key := pairSlice[0]
|
||||
if len(pairSlice) == 2 {
|
||||
value := pairSlice[1]
|
||||
orderedMap.Set(key, value)
|
||||
} else {
|
||||
// error cases are already filtered out from above part
|
||||
removalList = append(removalList, key[:len(key)-1])
|
||||
}
|
||||
}
|
||||
|
||||
return orderedMap, removalList, nil
|
||||
}
|
||||
|
||||
func MapFromArrayAllowingSingles(arr []string, delimiter string) (map[string]string, error) {
|
||||
return mapFromArray(arr, delimiter, true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,18 @@ func testMapFromArray(t *testing.T, input []string, delimiter string, expected m
|
|||
assert.DeepEqual(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestKeyValuePairListAndRemovalListFromArray(t *testing.T) {
|
||||
testKeyValuePairListAndRemovalListFromArray(t, []string{"add=value"}, "=", [][]string{{"add", "value"}}, []string{})
|
||||
testKeyValuePairListAndRemovalListFromArray(t, []string{"add=value", "remove-"}, "=", [][]string{{"add", "value"}}, []string{"remove"})
|
||||
}
|
||||
|
||||
func testKeyValuePairListAndRemovalListFromArray(t *testing.T, input []string, delimiter string, expectedKVList [][]string, expectedList []string) {
|
||||
actualKVList, actualList, err := OrderedMapAndRemovalListFromArray(input, delimiter)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, NewOrderedMapWithKVStrings(expectedKVList), actualKVList)
|
||||
assert.DeepEqual(t, expectedList, actualList)
|
||||
}
|
||||
|
||||
func TestMapFromArrayNoDelimiter(t *testing.T) {
|
||||
input := []string{"badvalue"}
|
||||
_, err := MapFromArray(input, "+")
|
||||
|
|
|
|||
Loading…
Reference in New Issue