diff --git a/docs/cmd/kn_service_create.md b/docs/cmd/kn_service_create.md index 852144da28..c5b1934898 100644 --- a/docs/cmd/kn_service_create.md +++ b/docs/cmd/kn_service_create.md @@ -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) ``` diff --git a/docs/cmd/kn_service_update.md b/docs/cmd/kn_service_update.md index 98f571636a..092dedfdb8 100644 --- a/docs/cmd/kn_service_update.md +++ b/docs/cmd/kn_service_update.md @@ -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) ``` diff --git a/pkg/kn/commands/flags/traffic.go b/pkg/kn/commands/flags/traffic.go index 89b0a7ad9c..4675e7e543 100644 --- a/pkg/kn/commands/flags/traffic.go +++ b/pkg/kn/commands/flags/traffic.go @@ -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 { diff --git a/pkg/kn/commands/service/configuration_edit_flags.go b/pkg/kn/commands/service/configuration_edit_flags.go index 587a0b345c..53f01929e1 100644 --- a/pkg/kn/commands/service/configuration_edit_flags.go +++ b/pkg/kn/commands/service/configuration_edit_flags.go @@ -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 diff --git a/pkg/kn/commands/service/create_mock_test.go b/pkg/kn/commands/service/create_mock_test.go index b8a95f1c9c..3fd7831ba1 100644 --- a/pkg/kn/commands/service/create_mock_test.go +++ b/pkg/kn/commands/service/create_mock_test.go @@ -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{ diff --git a/pkg/kn/commands/service/service_update_mock_test.go b/pkg/kn/commands/service/service_update_mock_test.go index 804b6da57c..2023f7ad8d 100644 --- a/pkg/kn/commands/service/service_update_mock_test.go +++ b/pkg/kn/commands/service/service_update_mock_test.go @@ -130,3 +130,1365 @@ func TestServiceUpdateAnnotationsMock(t *testing.T) { r.Validate() } + +func TestServiceUpdateEnvFromAddingWithConfigMap(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name", + }, + }, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "new-name", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--env-from", "config-map:existing-name", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "config-map:new-name", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateEnvFromRemovalWithConfigMap(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-2", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-3", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-4", + }, + }, + }, + } + + // prepare updated state + updatedService1 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService1) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-4", + }, + }, + }, + } + + // prepare updated state + updatedService2 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService2) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-4", + }, + }, + }, + } + + // prepare updated state + updatedService3 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService3) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = nil + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService1, nil) + r.GetService(svcName, updatedService1, nil) + //r.UpdateService(updatedService2, nil) // since an error happens, update is not triggered here + r.GetService(svcName, updatedService2, nil) + r.UpdateService(updatedService3, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--env-from", "config-map:existing-name-1", + "--env-from", "config-map:existing-name-2", + "--env-from", "cm:existing-name-3", + "--env-from", "config-map:existing-name-4", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "config-map:existing-name-1-", + "--env-from", "cm:existing-name-2-", + "--env-from", "config-map:existing-name-3-", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + // empty string + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "config-map:-", + "--async", "--revision-name=", + ) + assert.Error(t, err, "the name of config-map cannot be an empty string") + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "cm:existing-name-4-", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateEnvFromRemovalWithEmptyName(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + } + + // prepare updated state + updatedService1 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService1) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-2", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService1, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--env-from", "config-map:existing-name-1", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + _, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "-", + "--async", "--revision-name=", + ) + assert.Error(t, err, "\"-\" is not a valid value for \"--env-from\"") + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "config-map:existing-name-2", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) +} + +func TestServiceUpdateEnvFromExistingWithConfigMap(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-2", + }, + }, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-2", + }, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "new-name", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--env-from", "config-map:existing-name-1", + "--env-from", "config-map:existing-name-2", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "config-map:existing-name-1", + "--env-from", "config-map:new-name", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateEnvFromAddingWithSecret(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name", + }, + }, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "new-name", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--env-from", "secret:existing-name", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "sc:new-name", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} +func TestServiceUpdateEnvFromRemovalWithSecret(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-2", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-3", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-4", + }, + }, + }, + } + + // prepare updated state + updatedService1 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService1) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-4", + }, + }, + }, + } + + // prepare updated state + updatedService2 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService2) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-4", + }, + }, + }, + } + + // prepare updated state + updatedService3 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService3) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = nil + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService1, nil) + r.GetService(svcName, updatedService1, nil) + //r.UpdateService(updatedService2, nil) // since an error happens, update is not triggered here + r.GetService(svcName, updatedService2, nil) + r.UpdateService(updatedService3, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--env-from", "sc:existing-name-1", + "--env-from", "secret:existing-name-2", + "--env-from", "sc:existing-name-3", + "--env-from", "secret:existing-name-4", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "secret:existing-name-1-", + "--env-from", "sc:existing-name-2-", + "--env-from", "secret:existing-name-3-", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + // empty string + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "secret:-", + "--async", "--revision-name=", + ) + assert.Error(t, err, "the name of secret cannot be an empty string") + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "sc:existing-name-4-", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateEnvFromExistingWithSecret(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-2", + }, + }, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-1", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-name-2", + }, + }, + }, + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "new-name", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--env-from", "sc:existing-name-1", + "--env-from", "secret:existing-name-2", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--env-from", "secret:existing-name-1", + "--env-from", "secret:new-name", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateWithAddingVolume(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: "vol-1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-1", + }, + }, + }, + }, + { + Name: "vol-2", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-1", + }, + }, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: "vol-1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-1", + }, + }, + }, + }, + { + Name: "vol-2", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-1", + }, + }, + }, + { + Name: "vol-3", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-2", + }, + }, + }, + }, + { + Name: "vol-4", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-2", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--volume", "vol-1=cm:existing-config-map-1", + "--volume", "vol-2=secret:existing-secret-1", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--volume", "vol-3=cm:existing-config-map-2", + "--volume", "vol-4=secret:existing-secret-2", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateWithUpdatingVolume(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: "vol-1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-1", + }, + }, + }, + }, + { + Name: "vol-2", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-1", + }, + }, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: "vol-1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-3", + }, + }, + }, + }, + { + Name: "vol-2", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-3", + }, + }, + }, + { + Name: "vol-3", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-2", + }, + }, + }, + }, + { + Name: "vol-4", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-2", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--volume", "vol-1=cm:existing-config-map-1", + "--volume", "vol-2=secret:existing-secret-1", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--volume", "vol-1=cm:existing-config-map-3", + "--volume", "vol-2=secret:existing-secret-3", + "--volume", "vol-3=cm:existing-config-map-2", + "--volume", "vol-4=secret:existing-secret-2", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateWithRemovingVolume(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: "vol-1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-1", + }, + }, + }, + }, + { + Name: "vol-2", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-1", + }, + }, + }, + { + Name: "vol-3", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-2", + }, + }, + }, + }, + { + Name: "vol-4", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-2", + }, + }, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: "vol-1", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "existing-config-map-1", + }, + }, + }, + }, + { + Name: "vol-4", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "existing-secret-2", + }, + }, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--volume", "vol-1=cm:existing-config-map-1", + "--volume", "vol-2=secret:existing-secret-1", + "--volume", "vol-3=cm:existing-config-map-2", + "--volume", "vol-4=secret:existing-secret-2", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--volume", "vol-3-", + "--volume", "vol-2-", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateWithAddingMount(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path"), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map-name", + }, + }, + }, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path"), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "secret-name", + }, + }, + }, + } + + template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path"), + MountPath: "/mount/config-map-path", + ReadOnly: true, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path"), + MountPath: "/mount/secret-path", + ReadOnly: true, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--mount", "/mount/config-map-path=cm:config-map-name", + "--mount", "/mount/secret-path=secret:secret-name", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateWithUpdatingMount(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path"), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map-name-1", + }, + }, + }, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path"), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "secret-name-1", + }, + }, + }, + } + + template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path"), + MountPath: "/mount/config-map-path", + ReadOnly: true, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path"), + MountPath: "/mount/secret-path", + ReadOnly: true, + }, + } + + // prepare updated state + updatedService := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path"), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map-name-2", + }, + }, + }, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path"), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "secret-name-2", + }, + }, + }, + } + + template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path"), + MountPath: "/mount/config-map-path", + ReadOnly: true, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path"), + MountPath: "/mount/secret-path", + ReadOnly: true, + }, + } + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--mount", "/mount/config-map-path=cm:config-map-name-1", + "--mount", "/mount/secret-path=secret:secret-name-1", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + // Original orderedness should be kept after updating even though the orders + // between updating flags are not the same with the original one + output, err = executeServiceCommand(client, + "update", svcName, + "--mount", "/mount/secret-path=secret:secret-name-2", + "--mount", "/mount/config-map-path=cm:config-map-name-2", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} + +func TestServiceUpdateWithRemovingMount(t *testing.T) { + client := knclient.NewMockKnClient(t) + svcName := "svc1" + // prepare original state + newService := getService(svcName) + template, err := servinglib.RevisionTemplateOfService(newService) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path-1"), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map-name-1", + }, + }, + }, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path-1"), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "secret-name-1", + }, + }, + }, + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path-2"), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map-name-2", + }, + }, + }, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path-2"), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "secret-name-2", + }, + }, + }, + { + Name: "custom-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map", + }, + }, + }, + }, + } + + template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path-1"), + MountPath: "/mount/config-map-path-1", + ReadOnly: true, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path-1"), + MountPath: "/mount/secret-path-1", + ReadOnly: true, + }, + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path-2"), + MountPath: "/mount/config-map-path-2", + ReadOnly: true, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path-2"), + MountPath: "/mount/secret-path-2", + ReadOnly: true, + }, + { + Name: "custom-vol", + MountPath: "/mount/custom-path", + ReadOnly: true, + }, + } + + // prepare updated state + updatedService1 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService1) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + template.Spec.Volumes = []corev1.Volume{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path-1"), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map-name-1", + }, + }, + }, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path-2"), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "secret-name-2", + }, + }, + }, + { + Name: "custom-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map", + }, + }, + }, + }, + } + + template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ + { + Name: servinglib.GenerateVolumeName("/mount/config-map-path-1"), + MountPath: "/mount/config-map-path-1", + ReadOnly: true, + }, + { + Name: servinglib.GenerateVolumeName("/mount/secret-path-2"), + MountPath: "/mount/secret-path-2", + ReadOnly: true, + }, + { + Name: "custom-vol", + MountPath: "/mount/custom-path", + ReadOnly: true, + }, + } + + // prepare updated state + updatedService2 := getService(svcName) + template, err = servinglib.RevisionTemplateOfService(updatedService2) + assert.NilError(t, err) + template.Spec.GetContainer().Image = "gcr.io/foo/bar:baz" + template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"} + + r := client.Recorder() + r.GetService(svcName, nil, errors.NewNotFound(v1alpha1.Resource("service"), svcName)) + r.CreateService(newService, nil) + r.GetService(svcName, newService, nil) + r.UpdateService(updatedService1, nil) + r.GetService(svcName, updatedService1, nil) + r.UpdateService(updatedService2, nil) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--mount", "/mount/config-map-path-1=cm:config-map-name-1", + "--mount", "/mount/secret-path-1=secret:secret-name-1", + "--mount", "/mount/config-map-path-2=cm:config-map-name-2", + "--mount", "/mount/secret-path-2=secret:secret-name-2", + "--mount", "/mount/custom-path=custom-vol", + "--volume", "custom-vol=cm:config-map", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + template.Spec.Volumes = []corev1.Volume{ + { + Name: "custom-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "config-map", + }, + }, + }, + }, + } + + output, err = executeServiceCommand(client, + "update", svcName, + "--mount", "/mount/config-map-path-2-", + "--mount", "/mount/secret-path-1-", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--mount", "/mount/config-map-path-1-", + "--mount", "/mount/secret-path-2-", + "--mount", "/mount/custom-path-", + "--async", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} diff --git a/pkg/serving/config_changes.go b/pkg/serving/config_changes.go index 52a1ceebb5..75a50cb59e 100644 --- a/pkg/serving/config_changes.go +++ b/pkg/serving/config_changes.go @@ -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) +} diff --git a/pkg/serving/config_changes_test.go b/pkg/serving/config_changes_test.go index e81594c8ca..d7603fc6d8 100644 --- a/pkg/serving/config_changes_test.go +++ b/pkg/serving/config_changes_test.go @@ -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) + } +} + // // ========================================================================================================= diff --git a/pkg/util/orderedmap.go b/pkg/util/orderedmap.go new file mode 100644 index 0000000000..cdec09cdb6 --- /dev/null +++ b/pkg/util/orderedmap.go @@ -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 + } +} diff --git a/pkg/util/orderedmap_test.go b/pkg/util/orderedmap_test.go new file mode 100644 index 0000000000..d8724afc9f --- /dev/null +++ b/pkg/util/orderedmap_test.go @@ -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++ + } +} diff --git a/pkg/util/parsing_helper.go b/pkg/util/parsing_helper.go index eb41bc7539..74a68f4b4a 100644 --- a/pkg/util/parsing_helper.go +++ b/pkg/util/parsing_helper.go @@ -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) } diff --git a/pkg/util/parsing_helper_test.go b/pkg/util/parsing_helper_test.go index 686c18015e..0afc4f068b 100644 --- a/pkg/util/parsing_helper_test.go +++ b/pkg/util/parsing_helper_test.go @@ -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, "+")