Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service and Revision labels #342

Merged
merged 19 commits into from
Aug 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/cmd/kn_service_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ kn service create NAME --image IMAGE [flags]
--async Create service and don't wait for it to become ready.
--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.
-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-).
--force Create service forcefully, replaces existing service if any.
-h, --help help for create
--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-).
--limits-cpu string The limits on the requested CPU (e.g., 1000m).
--limits-memory string The limits on the requested memory (e.g., 1024Mi).
--max-scale int Maximal number of replicas.
Expand Down
3 changes: 2 additions & 1 deletion docs/cmd/kn_service_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ kn service update NAME [flags]
--async Update service and don't wait for it to become ready.
--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.
-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-).
-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-).
--limits-cpu string The limits on the requested CPU (e.g., 1000m).
--limits-memory string The limits on the requested memory (e.g., 1024Mi).
--max-scale int Maximal number of replicas.
Expand Down
55 changes: 42 additions & 13 deletions pkg/kn/commands/service/configuration_edit_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
package service

import (
"fmt"
"strings"

servinglib "github.com/knative/client/pkg/serving"
util "github.com/knative/client/pkg/util"
servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1"
errors "github.com/pkg/errors"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
Expand All @@ -35,6 +36,7 @@ type ConfigurationEditFlags struct {
ConcurrencyTarget int
ConcurrencyLimit int
Port int32
Labels []string
}

type ResourceFlags struct {
Expand All @@ -46,7 +48,8 @@ func (p *ConfigurationEditFlags) AddUpdateFlags(command *cobra.Command) {
command.Flags().StringVar(&p.Image, "image", "", "Image to run.")
command.Flags().StringArrayVarP(&p.Env, "env", "e", []string{},
"Environment variable to set. NAME=value; you may provide this flag "+
"any number of times to set multiple environment variables.")
"any number of times to set multiple environment variables. "+
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
command.Flags().StringVar(&p.RequestsFlags.CPU, "requests-cpu", "", "The requested CPU (e.g., 250m).")
command.Flags().StringVar(&p.RequestsFlags.Memory, "requests-memory", "", "The requested memory (e.g., 64Mi).")
command.Flags().StringVar(&p.LimitsFlags.CPU, "limits-cpu", "", "The limits on the requested CPU (e.g., 1000m).")
Expand All @@ -56,6 +59,10 @@ func (p *ConfigurationEditFlags) AddUpdateFlags(command *cobra.Command) {
command.Flags().IntVar(&p.ConcurrencyTarget, "concurrency-target", 0, "Recommendation for when to scale up based on the concurrent number of incoming request. Defaults to --concurrency-limit when given.")
command.Flags().IntVar(&p.ConcurrencyLimit, "concurrency-limit", 0, "Hard Limit of concurrent requests to be processed by a single replica.")
command.Flags().Int32VarP(&p.Port, "port", "p", 0, "The port where application listens on.")
command.Flags().StringArrayVarP(&p.Labels, "label", "l", []string{},
"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-).")
}

func (p *ConfigurationEditFlags) AddCreateFlags(command *cobra.Command) {
Expand All @@ -71,18 +78,22 @@ func (p *ConfigurationEditFlags) Apply(service *servingv1alpha1.Service, cmd *co
return err
}

envMap := map[string]string{}
for _, pairStr := range p.Env {
pairSlice := strings.SplitN(pairStr, "=", 2)
if len(pairSlice) <= 1 {
return fmt.Errorf(
"--env argument requires a value that contains the '=' character; got %s",
pairStr)
if cmd.Flags().Changed("env") {
envMap, err := util.MapFromArrayAllowingSingles(p.Env, "=")
if err != nil {
return errors.Wrap(err, "Invalid --env")
}
envToRemove := []string{}
for name := range envMap {
if strings.HasSuffix(name, "-") {
envToRemove = append(envToRemove, name[:len(name)-1])
delete(envMap, name)
}
}
err = servinglib.UpdateEnvVars(template, envMap, envToRemove)
if err != nil {
return err
}
envMap[pairSlice[0]] = pairSlice[1]
}
if err := servinglib.UpdateEnvVars(template, envMap); err != nil {
return err
}

if cmd.Flags().Changed("image") {
Expand Down Expand Up @@ -139,6 +150,24 @@ func (p *ConfigurationEditFlags) Apply(service *servingv1alpha1.Service, cmd *co
}
}

if cmd.Flags().Changed("label") {
labelsMap, err := util.MapFromArrayAllowingSingles(p.Labels, "=")
if err != nil {
return errors.Wrap(err, "Invalid --label")
}
labelsToRemove := []string{}
for key := range labelsMap {
if strings.HasSuffix(key, "-") {
labelsToRemove = append(labelsToRemove, key[:len(key)-1])
delete(labelsMap, key)
}
}
err = servinglib.UpdateLabels(service, template, labelsMap, labelsToRemove)
if err != nil {
return err
}
}

return nil
}

Expand Down
55 changes: 55 additions & 0 deletions pkg/kn/commands/service/service_create_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ import (

"github.com/knative/pkg/apis"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/knative/serving/pkg/apis/serving/v1alpha1"

servinglib "github.com/knative/client/pkg/serving"
knclient "github.com/knative/client/pkg/serving/v1alpha1"

"github.com/knative/client/pkg/util"
Expand Down Expand Up @@ -53,6 +56,58 @@ func TestServiceCreateImageMock(t *testing.T) {
r.Validate()
}

func TestServiceCreateLabel(t *testing.T) {
client := knclient.NewMockKnClient(t)

r := client.Recorder()
r.GetService("foo", nil, errors.NewNotFound(v1alpha1.Resource("service"), "foo"))

service := getService("foo")
expected := map[string]string{
"a": "mouse",
"b": "cookie",
"empty": "",
}
service.ObjectMeta.Labels = expected
template, err := servinglib.RevisionTemplateOfService(service)
assert.NilError(t, err)
template.ObjectMeta.Labels = expected
template.Spec.DeprecatedContainer.Image = "gcr.io/foo/bar:baz"
r.CreateService(service, nil)

output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "-l", "a=mouse", "--label", "b=cookie", "--label=empty", "--async")
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{
Name: name,
Namespace: "default",
},
Spec: v1alpha1.ServiceSpec{
DeprecatedRunLatest: &v1alpha1.RunLatestType{
Configuration: v1alpha1.ConfigurationSpec{
DeprecatedRevisionTemplate: &v1alpha1.RevisionTemplateSpec{
Spec: v1alpha1.RevisionSpec{
DeprecatedContainer: &corev1.Container{
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{},
Requests: corev1.ResourceList{},
},
},
},
},
},
},
},
}
return service
}

func getServiceWithUrl(name string, urlName string) *v1alpha1.Service {
service := v1alpha1.Service{}
url, _ := apis.ParseURL(urlName)
Expand Down
8 changes: 5 additions & 3 deletions pkg/kn/commands/service/service_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func TestServiceCreateImageSync(t *testing.T) {

func TestServiceCreateEnv(t *testing.T) {
action, created, _, err := fakeServiceCreate([]string{
"service", "create", "foo", "--image", "gcr.io/foo/bar:baz", "-e", "A=DOGS", "--env", "B=WOLVES", "--async"}, false, false)
"service", "create", "foo", "--image", "gcr.io/foo/bar:baz", "-e", "A=DOGS", "--env", "B=WOLVES", "--env=EMPTY", "--async"}, false, false)

if err != nil {
t.Fatal(err)
Expand All @@ -167,8 +167,10 @@ func TestServiceCreateEnv(t *testing.T) {
}

expectedEnvVars := map[string]string{
"A": "DOGS",
"B": "WOLVES"}
"A": "DOGS",
"B": "WOLVES",
"EMPTY": "",
}

template, err := servinglib.RevisionTemplateOfService(created)
actualEnvVars, err := servinglib.EnvToMap(template.Spec.DeprecatedContainer.Env)
Expand Down
60 changes: 59 additions & 1 deletion pkg/kn/commands/service/service_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,15 @@ func TestServiceUpdateEnv(t *testing.T) {
if err != nil {
t.Fatal(err)
}
template.Spec.DeprecatedContainer.Env = []corev1.EnvVar{
{Name: "EXISTING", Value: "thing"},
{Name: "OTHEREXISTING"},
}

servinglib.UpdateImage(template, "gcr.io/foo/bar:baz")

action, updated, _, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "-e", "TARGET=Awesome", "--async"}, false)
"service", "update", "foo", "-e", "TARGET=Awesome", "--env", "EXISTING-", "--env=OTHEREXISTING-=whatever", "--async"}, false)

if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -374,6 +378,60 @@ func TestServiceUpdateRequestsLimitsCPU_and_Memory(t *testing.T) {
}
}

func TestServiceUpdateLabelWhenEmpty(t *testing.T) {
original := newEmptyService()

action, updated, _, err := fakeServiceUpdate(original, []string{
"service", "update", "foo", "-l", "a=mouse", "--label", "b=cookie", "-l=single", "--async"}, false)

if err != nil {
t.Fatal(err)
} else if !action.Matches("update", "services") {
t.Fatalf("Bad action %v", action)
}

expected := map[string]string{
"a": "mouse",
"b": "cookie",
"single": "",
}
actual := updated.ObjectMeta.Labels
assert.DeepEqual(t, expected, actual)

template, err := servinglib.RevisionTemplateOfService(updated)
assert.NilError(t, err)
actual = template.ObjectMeta.Labels
assert.DeepEqual(t, expected, actual)
}

func TestServiceUpdateLabelExisting(t *testing.T) {
original := newEmptyService()
original.ObjectMeta.Labels = map[string]string{"already": "here", "tobe": "removed"}
originalTemplate, _ := servinglib.RevisionTemplateOfService(original)
originalTemplate.ObjectMeta.Labels = map[string]string{"already": "here", "tobe": "removed"}

action, updated, _, err := fakeServiceUpdate(original, []string{
"service", "update", "foo", "-l", "already=gone", "--label=tobe-", "--label", "b=", "--async"}, false)

if err != nil {
t.Fatal(err)
} else if !action.Matches("update", "services") {
t.Fatalf("Bad action %v", action)
}

expected := map[string]string{
"already": "gone",
"b": "",
}
actual := updated.ObjectMeta.Labels
assert.DeepEqual(t, expected, actual)

template, err := servinglib.RevisionTemplateOfService(updated)
assert.NilError(t, err)
actual = template.ObjectMeta.Labels
assert.DeepEqual(t, expected, actual)
}

func newEmptyService() *v1alpha1.Service {
return &v1alpha1.Service{
TypeMeta: metav1.TypeMeta{
Expand Down
37 changes: 35 additions & 2 deletions pkg/serving/config_changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ import (
// UpdateEnvVars gives the configuration all the env var values listed in the given map of
// vars. Does not touch any environment variables not mentioned, but it can add
// new env vars and change the values of existing ones.
func UpdateEnvVars(template *servingv1alpha1.RevisionTemplateSpec, vars map[string]string) error {
func UpdateEnvVars(template *servingv1alpha1.RevisionTemplateSpec, toUpdate map[string]string, toRemove []string) error {
container, err := ContainerOfRevisionTemplate(template)
if err != nil {
return err
}
container.Env = updateEnvVarsFromMap(container.Env, vars)
envVars := updateEnvVarsFromMap(container.Env, toUpdate)
container.Env = removeEnvVars(envVars, toRemove)
return nil
}

Expand Down Expand Up @@ -152,6 +153,26 @@ func UpdateResources(template *servingv1alpha1.RevisionTemplateSpec, requestsRes
return nil
}

// UpdateLabels updates the labels identically on a service and template.
// Does not overwrite the entire Labels field, only makes the requested updates
func UpdateLabels(service *servingv1alpha1.Service, template *servingv1alpha1.RevisionTemplateSpec, toUpdate map[string]string, toRemove []string) error {
if service.ObjectMeta.Labels == nil {
service.ObjectMeta.Labels = make(map[string]string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is whether the labels of the service or the labels of the revision template which should be set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably be decided in #328. I like the simplicity of --label setting both, but not sure if preventing users from setting separate labels on services and revision templates would be an issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My gut feeling is that people most often want to set labels for their pods, not the service. So we could settle on something like that:

  • --label mylabel=myvalue would set it on both, the service and the pod
  • --label ^mylabel=myvalue would set it only on the Service (the "outer" entity)
  • --label :mylabel=myvalue would set it only on the revision template (the "inner" entity)

That would cover 90% of all use case by setting both, but allows for tuning, too. Not sure about te concrete characters (one should also check whether there are special shell meanings so that those chars can be used unescaped), but using any character which can not be used within label keys should be cool.

wdyt ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems at that point there might as well be separate --revision-label, --service-label , and --label flags. It would be much more explicit both in documentation and I think code that way. Plus, special characters change (slightly) across different shell environments and if this list is any indication, it may not be worth the time and effort to find and confirm which characters are valid in a reasonable number of shell environments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I too like the simplicity of —label for all labels as the object is already decided in the command group. Adding more complexity and notations to remember is unnecessary as it complicates the “language” of the CLI IMO.

Copy link
Contributor

@rhuss rhuss Aug 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems at that point there might as well be separate --revision-label, --service-label , and --label flags. It would be much more explicit both in the documentation and I think code that way. Plus, special characters change (slightly) across different shell environments and if this list is any indication, it may not be worth the time and effort to find and confirm which characters are valid in a reasonable number of shell environments.

This is always a tradeoff between options bloat and being too obfuscated. Currently, there is a tendency to add 1-5 new options per new features, and I'm doing my best to fight against the bloat. At least asking, whether there could be a compromise and whether all those options are needed (e.g. see #345 (comment) or #326 (review) or #282 (review) how to reduce the UX surface and so the cognitive load).

Of course, we should no go over the top, and using special chars like that might be the case here (although ^ and : are safe shell chars so no need to escape).

What's about that compromise: Let's implement --label to label both, service and template and wait on feedback if the other use cases (individual labelling of serving/template) are actually requested ?


From a single feature's PR POV adding 3 new options might not be a big deal, but its the sum which might lead to multi page help outputs which are really overwhelming (especially when alphabetically ordered, and --revision-label, --sevice-label and --label will apear on totally different positions in a list of 50+ options)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK on compromise!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: I'm starting to think we should implement our own flags groupings, which might help with this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spf13/cobra#836 has some discussion on flag groupings in help text. Unfortunately, it doesn’t look like there’s currently first class support for it.

}
if template.ObjectMeta.Labels == nil {
template.ObjectMeta.Labels = make(map[string]string)
}
for key, value := range toUpdate {
service.ObjectMeta.Labels[key] = value
template.ObjectMeta.Labels[key] = value
}
for _, key := range toRemove {
delete(service.ObjectMeta.Labels, key)
delete(template.ObjectMeta.Labels, key)
}
aaron-lerner marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

// =======================================================================================

func updateEnvVarsFromMap(env []corev1.EnvVar, vars map[string]string) []corev1.EnvVar {
Expand All @@ -176,3 +197,15 @@ func updateEnvVarsFromMap(env []corev1.EnvVar, vars map[string]string) []corev1.
}
return env
}

func removeEnvVars(env []corev1.EnvVar, toRemove []string) []corev1.EnvVar {
for _, name := range toRemove {
for i, envVar := range env {
if envVar.Name == name {
env = append(env[:i], env[i+1:]...)
break
}
}
}
return env
}
Loading