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 9 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
1 change: 1 addition & 0 deletions docs/cmd/kn_service_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ kn service create NAME --image IMAGE [flags]
--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; provide an empty string for the value to remove a label. You may provide this flag any number of times to set multiple labels.
--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
1 change: 1 addition & 0 deletions docs/cmd/kn_service_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ kn service update NAME [flags]
-e, --env stringArray Environment variable to set. NAME=value; you may provide this flag any number of times to set multiple environment variables.
-h, --help help for update
--image string Image to run.
-l, --label stringArray Service label to set. NAME=value; provide an empty string for the value to remove a label. You may provide this flag any number of times to set multiple labels.
--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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ require (
github.com/mitchellh/go-homedir v1.1.0
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/pkg/errors v0.8.1
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.1
Expand Down
34 changes: 34 additions & 0 deletions pkg/kn/commands/parsing_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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 commands
aaron-lerner marked this conversation as resolved.
Show resolved Hide resolved

import (
"fmt"
"strings"
)

// MapFromArray takes an array of strings where each item is a (key, value) pair
// separated by a delimiter and returns a map where keys are mapped to their respsective values.
func MapFromArray(arr []string, delimiter string) (map[string]string, error) {
returnMap := map[string]string{}
for _, pairStr := range arr {
pairSlice := strings.SplitN(pairStr, delimiter, 2)
if len(pairSlice) <= 1 {
return nil, fmt.Errorf("Argument requires a value that contains the %q character; got %q", delimiter, pairStr)
}
returnMap[pairSlice[0]] = pairSlice[1]
}
return returnMap, nil
}
49 changes: 49 additions & 0 deletions pkg/kn/commands/parsing_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright © 2018 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 commands

import (
"reflect"
"testing"

"gotest.tools/assert"
)

func TestMapFromArray(t *testing.T) {
aaron-lerner marked this conversation as resolved.
Show resolved Hide resolved
testMapFromArray(t, []string{"good=value"}, "=", map[string]string{"good": "value"})
testMapFromArray(t, []string{"multi=value", "other=value"}, "=", map[string]string{"multi": "value", "other": "value"})
testMapFromArray(t, []string{"over|write", "over|written"}, "|", map[string]string{"over": "written"})
testMapFromArray(t, []string{"only,split,once"}, ",", map[string]string{"only": "split,once"})
}

func testMapFromArray(t *testing.T, input []string, delimiter string, expected map[string]string) {
actual, err := MapFromArray(input, delimiter)
assert.NilError(t, err)
if !reflect.DeepEqual(expected, actual) {
aaron-lerner marked this conversation as resolved.
Show resolved Hide resolved
t.Fatalf("Map did not match expected: %s\nFound: %s", expected, actual)
}
}

func TestMapFromArrayNoDelimiter(t *testing.T) {
input := []string{"good=value", "badvalue"}
_, err := MapFromArray(input, "=")
assert.ErrorContains(t, err, "Argument requires")
}

func TestMapFromArrayEmptyValue(t *testing.T) {
input := []string{""}
_, err := MapFromArray(input, "=")
assert.ErrorContains(t, err, "Argument requires")
aaron-lerner marked this conversation as resolved.
Show resolved Hide resolved
}
37 changes: 23 additions & 14 deletions pkg/kn/commands/service/configuration_edit_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
package service

import (
"fmt"
"strings"

commands "github.com/knative/client/pkg/kn/commands"
servinglib "github.com/knative/client/pkg/serving"
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 +34,7 @@ type ConfigurationEditFlags struct {
ConcurrencyTarget int
ConcurrencyLimit int
Port int32
Labels []string
}

type ResourceFlags struct {
Expand All @@ -56,6 +56,7 @@ 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; provide an empty string for the value to remove a label. You may provide this flag any number of times to set multiple labels.")
}

func (p *ConfigurationEditFlags) AddCreateFlags(command *cobra.Command) {
Expand All @@ -71,18 +72,15 @@ 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 := commands.MapFromArray(p.Env, "=")
if err != nil {
return errors.Wrap(err, "Invalid --env")
}
err = servinglib.UpdateEnvVars(template, envMap)
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 +137,17 @@ func (p *ConfigurationEditFlags) Apply(service *servingv1alpha1.Service, cmd *co
}
}

if cmd.Flags().Changed("label") {
labelMap, err := commands.MapFromArray(p.Labels, "=")
if err != nil {
return errors.Wrap(err, "Invalid --label")
}
err = servinglib.UpdateServiceLabels(service, labelMap)
if err != nil {
return err
}
}

return nil
}

Expand Down
27 changes: 27 additions & 0 deletions pkg/kn/commands/service/service_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,30 @@ func TestServiceCreateEnvForce(t *testing.T) {
t.Fatalf("wrong output: %s", output)
}
}

func TestServiceCreateLabel(t *testing.T) {
action, created, _, err := fakeServiceCreate([]string{
aaron-lerner marked this conversation as resolved.
Show resolved Hide resolved
"service", "create", "foo", "--image", "gcr.io/foo/bar:baz", "-l", "a=mouse", "--label", "b=cookie", "--async"}, false, false)
aaron-lerner marked this conversation as resolved.
Show resolved Hide resolved

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

expectedLabels := map[string]string{
"a": "mouse",
"b": "cookie"}
actualLabels := created.ObjectMeta.Labels

template, err := servinglib.RevisionTemplateOfService(created)
if err != nil {
t.Fatal(err)
} else if template.Spec.DeprecatedContainer.Image != "gcr.io/foo/bar:baz" {
t.Fatalf("wrong image set: %v", template.Spec.DeprecatedContainer.Image)
} else if !reflect.DeepEqual(
actualLabels,
expectedLabels) {
t.Fatalf("wrong labels %v", created.ObjectMeta.Labels)
}
}
45 changes: 45 additions & 0 deletions pkg/kn/commands/service/service_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,51 @@ 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", "--async"}, false)

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

expectedLabels := map[string]string{
"a": "mouse",
"b": "cookie"}
actualLabels := updated.ObjectMeta.Labels

if !reflect.DeepEqual(actualLabels, expectedLabels) {
t.Fatalf("wrong labels %v", actualLabels)
}
}

func TestServiceUpdateLabelExisting(t *testing.T) {
original := newEmptyService()
original.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=cookie", "--async"}, false)

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

expectedLabels := map[string]string{
"already": "gone",
"b": "cookie"}
actualLabels := updated.ObjectMeta.Labels

if !reflect.DeepEqual(actualLabels, expectedLabels) {
t.Fatalf("wrong labels %v", actualLabels)
}
}

func newEmptyService() *v1alpha1.Service {
return &v1alpha1.Service{
TypeMeta: metav1.TypeMeta{
Expand Down
16 changes: 16 additions & 0 deletions pkg/serving/config_changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,22 @@ func UpdateResources(template *servingv1alpha1.RevisionTemplateSpec, requestsRes
return nil
}

// UpdateServiceLabels updates the labels on a service
func UpdateServiceLabels(service *servingv1alpha1.Service, vars map[string]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.

}
for key, value := range vars {
// Delete the label if passed an empty string, otherwise set or update it
if value == "" {
delete(service.ObjectMeta.Labels, key)
} else {
service.ObjectMeta.Labels[key] = value
}
}
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 Down
70 changes: 70 additions & 0 deletions pkg/serving/config_changes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestUpdateAutoscalingAnnotations(t *testing.T) {
Expand Down Expand Up @@ -270,6 +271,53 @@ func testUpdateEnvVarsBoth(t *testing.T, template *servingv1alpha1.RevisionTempl
}
}

func TestUpdateLabelsNew(t *testing.T) {
service := getV1alpha1Service()
labels := map[string]string{
"a": "foo",
"b": "bar",
}
err := UpdateServiceLabels(service, labels)
assert.NilError(t, err)
actual := service.ObjectMeta.Labels
if !reflect.DeepEqual(labels, actual) {
t.Fatalf("Labels did not match expected %v found %v", labels, actual)
}
}

func TestUpdateLabelsExisting(t *testing.T) {
service := getV1alpha1Service()
service.ObjectMeta.Labels = map[string]string{"a": "foo"}
labels := map[string]string{
"a": "notfood",
"b": "bar",
}
err := UpdateServiceLabels(service, labels)
assert.NilError(t, err)
actual := service.ObjectMeta.Labels
if !reflect.DeepEqual(labels, actual) {
t.Fatalf("Labels did not match expected %v found %v", labels, actual)
}
}

func TestUpdateLabelsRemoveExisting(t *testing.T) {
service := getV1alpha1Service()
service.ObjectMeta.Labels = map[string]string{"a": "foo", "b": "bar"}
labels := map[string]string{
"a": "foo",
"b": "",
}
err := UpdateServiceLabels(service, labels)
assert.NilError(t, err)
expected := map[string]string{
"a": "foo",
}
actual := service.ObjectMeta.Labels
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("Labels did not match expected %v found %v", expected, actual)
}
}

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

func getV1alpha1RevisionTemplateWithOldFields() (*servingv1alpha1.RevisionTemplateSpec, *corev1.Container) {
Expand All @@ -296,6 +344,28 @@ func getV1alpha1Config() (*servingv1alpha1.RevisionTemplateSpec, *corev1.Contain
return template, &containers[0]
}

func getV1alpha1Service() *servingv1alpha1.Service {
template, _ := getV1alpha1RevisionTemplateWithOldFields()
service := &servingv1alpha1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "knative.dev/v1alph1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
},
Spec: servingv1alpha1.ServiceSpec{
DeprecatedRunLatest: &servingv1alpha1.RunLatestType{
Configuration: servingv1alpha1.ConfigurationSpec{
DeprecatedRevisionTemplate: template,
},
},
},
}
return service
}

func assertNoV1alpha1Old(t *testing.T, template *servingv1alpha1.RevisionTemplateSpec) {
if template.Spec.DeprecatedContainer != nil {
t.Error("Assuming only new v1alpha1 fields but found spec.container")
Expand Down