Add support for envFrom and volumeMounts (#393)

* Add support for envFrom, volumeMounts, serviceAccountName, and imagePullPolicy

* Add comments for added functions

* Add test functions for --env-from

* Add test functions for --volume-mount

* Add test functions for --service-account-name and --image-pull-policy

* Add testing functions to cover the case "service update --env-from"

* Add test functions to cover the case "service update --volume-mount"

* Add missing test functions for config_changes.go

* Add testing functions for pkg/util/parsing_helper.go

* Fix a bug on a test case, caused by ignoring random orderedness of map

* Remove image-pull-policy because it is not supported by Knative

* Fix comments to clarify it as well as to fix a typo

* Remove service-account-name flags in order to submit it as a seperate PR

* Split --volume-mount flag into --volume-mount and --volume flags in order to enable multiple times mounting for the same volume

* Change the name of local variable to simplify

* Update docs

* Change the flag "--volume-mount" into "--mount", and fix it to make volume automatically when the config-map or secret is directley used. In addition, the test cases are changed to new unit test style. To keep the original orderedness given via flags, OrderedMap implementation is added as well

* Fix usage descriptions for the mount flag

* Factor out the name existence checking from createEnvFromSource

* Fix the usage description for --mount

* Sanitize a generated volume name
This commit is contained in:
Ingwon Song 2019-11-06 04:15:03 -08:00 committed by Knative Prow Robot
parent e6f15f9b58
commit 9d759ca6e4
12 changed files with 2546 additions and 6 deletions

View File

@ -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)
```

View File

@ -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)
```

View File

@ -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 {

View File

@ -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

View File

@ -113,6 +113,288 @@ func TestServiceCreateLabel(t *testing.T) {
r.Validate()
}
func TestServiceCreateWithEnvFromConfigMap(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{
{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "config-map-name",
},
},
},
}
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "config-map:config-map-name", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func TestServiceCreateWithEnvFromConfigMapRemoval(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.GetContainer().EnvFrom = nil
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "config-map:config-map-name-", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func TestServiceCreateWithEnvFromEmptyRemoval(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.GetContainer().EnvFrom = nil
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
_, err = executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "-", "--async", "--revision-name=")
assert.Error(t, err, "\"-\" is not a valid value for \"--env-from\"")
}
func TestServiceCreateWithEnvFromSecret(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.GetContainer().EnvFrom = []corev1.EnvFromSource{
{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "secret-name",
},
},
},
}
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "secret:secret-name", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func TestServiceCreateWithEnvFromSecretRemoval(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.GetContainer().EnvFrom = nil
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--env-from", "secret:secret-name-", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func TestServiceCreateWithVolumeAndMountConfigMap(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.Volumes = []corev1.Volume{
{
Name: "volume-name",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "config-map-name",
},
},
},
},
}
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
{
Name: "volume-name",
MountPath: "/mount/path",
ReadOnly: true,
},
}
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
"--mount", "/mount/path=volume-name", "--volume", "volume-name=cm:config-map-name", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func TestServiceCreateWithMountConfigMap(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.Volumes = []corev1.Volume{
{
Name: servinglib.GenerateVolumeName("/mount/path"),
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "config-map-name",
},
},
},
},
}
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
{
Name: servinglib.GenerateVolumeName("/mount/path"),
MountPath: "/mount/path",
ReadOnly: true,
},
}
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
"--mount", "/mount/path=cm:config-map-name", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func TestServiceCreateWithVolumeAndMountSecret(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.Volumes = []corev1.Volume{
{
Name: "volume-name",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "secret-name",
},
},
},
}
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
{
Name: "volume-name",
MountPath: "/mount/path",
ReadOnly: true,
},
}
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
"--mount", "/mount/path=volume-name", "--volume", "volume-name=secret:secret-name", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func TestServiceCreateWithMountSecret(t *testing.T) {
client := knclient.NewMockKnClient(t)
r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))
service := getService("foo")
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.Spec.Volumes = []corev1.Volume{
{
Name: servinglib.GenerateVolumeName("/mount/path"),
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "secret-name",
},
},
},
}
template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{
{
Name: servinglib.GenerateVolumeName("/mount/path"),
MountPath: "/mount/path",
ReadOnly: true,
},
}
template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz"
template.Annotations = map[string]string{servinglib.UserImageAnnotationKey: "gcr.io/foo/bar:baz"}
r.CreateService(service, nil)
output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz",
"--mount", "/mount/path=sc:secret-name", "--async", "--revision-name=")
assert.NilError(t, err)
assert.Assert(t, util.ContainsAll(output, "created", "foo", "default"))
r.Validate()
}
func getService(name string) *v1alpha1.Service {
service := &v1alpha1.Service{
ObjectMeta: metav1.ObjectMeta{

File diff suppressed because it is too large Load Diff

View File

@ -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)
}

View File

@ -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)
}
}
//
// =========================================================================================================

147
pkg/util/orderedmap.go Normal file
View File

@ -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
}
}

View File

@ -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++
}
}

View File

@ -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)
}

View File

@ -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, "+")