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

Specify names on service update and generate names client-side. #282

Merged
merged 14 commits into from
Aug 14, 2019
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
| `kn service list` lists services sorted by alphabetical order
| https://github.com/knative/client/pull/330[#330]

| 🎁
| Add --revision-name flag
| https://github.com/knative/client/pull/282[#282]

|===

## v0.2.0 (2019-07-10)
Expand Down
3 changes: 3 additions & 0 deletions cmd/kn/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package main

import (
"fmt"
"math/rand"
"os"
"time"

"github.com/knative/client/pkg/kn/core"
"github.com/spf13/viper"
Expand All @@ -30,6 +32,7 @@ var err error

func main() {
defer cleanup()
rand.Seed(time.Now().UnixNano())
err = core.NewDefaultKnCommand().Execute()
if err != nil {
fmt.Fprintln(os.Stderr, err)
Expand Down
1 change: 1 addition & 0 deletions docs/cmd/kn_service_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ kn service create NAME --image IMAGE [flags]
-p, --port int32 The port where application listens on.
--requests-cpu string The requested CPU (e.g., 250m).
--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}}")
--wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 60)
```

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 @@ -42,6 +42,7 @@ kn service update NAME [flags]
-p, --port int32 The port where application listens on.
--requests-cpu string The requested CPU (e.g., 250m).
--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}}")
sixolet marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we describe what "Generation" is in our docs some place?

--wait-timeout int Seconds to wait before giving up on waiting for service to be ready. (default 60)
```

Expand Down
88 changes: 80 additions & 8 deletions pkg/kn/commands/service/configuration_edit_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,51 +27,98 @@ import (
)

type ConfigurationEditFlags struct {
// Direct field manipulation
Image string
Env []string
RequestsFlags, LimitsFlags ResourceFlags
ForceCreate bool
MinScale int
MaxScale int
ConcurrencyTarget int
ConcurrencyLimit int
Port int32
Labels []string
NamePrefix string
RevisionName string

// Preferences about how to do the action.
ForceCreate bool

// Bookkeeping
flags []string
}

type ResourceFlags struct {
CPU string
Memory string
}

func (p *ConfigurationEditFlags) AddUpdateFlags(command *cobra.Command) {
// markFlagMakesRevision indicates that a flag will create a new revision if you
// set it.
func (p *ConfigurationEditFlags) markFlagMakesRevision(f string) {
p.flags = append(p.flags, f)
}

// addSharedFlags adds the flags common between create & update.
func (p *ConfigurationEditFlags) addSharedFlags(command *cobra.Command) {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a comment to explain that "Shared" means that these flags are common between both create() and update() ?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's get this PR in before #345 so that #345 can add its flags in here too!

Copy link
Collaborator

Choose a reason for hiding this comment

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

@duglin We add traffic flags separately on service update command. IMO they don't need to show up in ConfigurationEditFlags as updating traffic block is not configuration-edit operation and only traffic flags change don't result into a new revision.

Copy link
Contributor

Choose a reason for hiding this comment

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

From a UX perspective, I'd like to be able to setup the traffic during "service create" - that's all I'm asking for.

command.Flags().StringVar(&p.Image, "image", "", "Image to run.")
p.markFlagMakesRevision("image")
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. "+
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
p.markFlagMakesRevision("env")
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).")
p.markFlagMakesRevision("requests-memory")
command.Flags().StringVar(&p.LimitsFlags.CPU, "limits-cpu", "", "The limits on the requested CPU (e.g., 1000m).")
command.Flags().StringVar(&p.LimitsFlags.Memory, "limits-memory", "", "The limits on the requested memory (e.g., 1024Mi).")
p.markFlagMakesRevision("limits-cpu")
command.Flags().StringVar(&p.LimitsFlags.Memory, "limits-memory", "",
"The limits on the requested memory (e.g., 1024Mi).")
p.markFlagMakesRevision("limits-memory")
command.Flags().IntVar(&p.MinScale, "min-scale", 0, "Minimal number of replicas.")
p.markFlagMakesRevision("min-scale")
command.Flags().IntVar(&p.MaxScale, "max-scale", 0, "Maximal number of replicas.")
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.")
p.markFlagMakesRevision("max-scale")
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.")
p.markFlagMakesRevision("concurrency-target")
command.Flags().IntVar(&p.ConcurrencyLimit, "concurrency-limit", 0,
"Hard Limit of concurrent requests to be processed by a single replica.")
p.markFlagMakesRevision("concurrency-limit")
command.Flags().Int32VarP(&p.Port, "port", "p", 0, "The port where application listens on.")
p.markFlagMakesRevision("port")
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-).")
p.markFlagMakesRevision("label")
command.Flags().StringVar(&p.RevisionName, "revision-name", "{{.Service}}-{{.Random 5}}-{{.Generation}}",
Copy link
Contributor

@duglin duglin Aug 9, 2019

Choose a reason for hiding this comment

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

this seems excessively long for on-screen help text :-)

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 inspired #351 . I think it's better to describe clearly.

Copy link
Contributor

Choose a reason for hiding this comment

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

only the first time, after that when people do --help it'll just annoy them

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hum. Roland suggested I add the part about the empty string. I think the part about the template is valuable, as it is not discoverable otherwise.

We could make it so, but we don't have a good place for that yet. I'd like to defer shortening it until we have a good place for other doc on options.

"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.")
p.markFlagMakesRevision("revision-name")
}

// AddUpdateFlags adds the flags specific to update.
func (p *ConfigurationEditFlags) AddUpdateFlags(command *cobra.Command) {
p.addSharedFlags(command)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The better way to handle this separation would be to implement the suggestion from @cppforlife #79 (comment) logged as issue at #110, that should go as separate PR though and shouldn't block this one.

}

// AddCreateFlags adds the flags specific to create
func (p *ConfigurationEditFlags) AddCreateFlags(command *cobra.Command) {
p.AddUpdateFlags(command)
command.Flags().BoolVar(&p.ForceCreate, "force", false, "Create service forcefully, replaces existing service if any.")
p.addSharedFlags(command)
command.Flags().BoolVar(&p.ForceCreate, "force", false,
"Create service forcefully, replaces existing service if any.")
command.MarkFlagRequired("image")
}

func (p *ConfigurationEditFlags) Apply(service *servingv1alpha1.Service, cmd *cobra.Command) error {
// Apply mutates the given service according to the flags in the command.
func (p *ConfigurationEditFlags) Apply(
sixolet marked this conversation as resolved.
Show resolved Hide resolved
service *servingv1alpha1.Service,
cmd *cobra.Command) error {

template, err := servinglib.RevisionTemplateOfService(service)
if err != nil {
Expand All @@ -96,6 +143,20 @@ func (p *ConfigurationEditFlags) Apply(service *servingv1alpha1.Service, cmd *co
}
}

name, err := servinglib.GenerateRevisionName(p.RevisionName, service)
if err != nil {
return err
}

if p.AnyMutation(cmd) {
err = servinglib.UpdateName(template, name)
if err == servinglib.ApiTooOldError && !cmd.Flags().Changed("revision-name") {
sixolet marked this conversation as resolved.
Show resolved Hide resolved
// Ignore the error if we don't support revision names and nobody
// explicitly asked for one.
} else if err != nil {
return err
}
}
if cmd.Flags().Changed("image") {
err = servinglib.UpdateImage(template, p.Image)
if err != nil {
Expand Down Expand Up @@ -194,3 +255,14 @@ func (p *ConfigurationEditFlags) computeResources(resourceFlags ResourceFlags) (

return resourceList, nil
}

// AnyMutation returns true if there are any revision template mutations in the
// command.
func (p *ConfigurationEditFlags) AnyMutation(cmd *cobra.Command) bool {
sixolet marked this conversation as resolved.
Show resolved Hide resolved
for _, flag := range p.flags {
if cmd.Flags().Changed(flag) {
return true
}
}
return false
}
110 changes: 110 additions & 0 deletions pkg/kn/commands/service/service_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"testing"

"gotest.tools/assert"
"gotest.tools/assert/cmp"

"github.com/knative/client/pkg/kn/commands"
servinglib "github.com/knative/client/pkg/serving"
Expand Down Expand Up @@ -166,6 +167,98 @@ func TestServiceUpdateImage(t *testing.T) {
}
}

func TestServiceUpdateRevisionNameExplicit(t *testing.T) {
orig := newEmptyServiceBetaAPIStyle()

template, err := servinglib.RevisionTemplateOfService(orig)
if err != nil {
t.Fatal(err)
}

template.Name = "foo-asdf"

// Test user provides prefix
action, updated, _, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "--revision-name", "foo-dogs", "--namespace", "bar", "--async"}, false)
assert.NilError(t, err)
if !action.Matches("update", "services") {
t.Fatalf("Bad action %v", action)
}
template, err = servinglib.RevisionTemplateOfService(updated)
assert.NilError(t, err)
assert.Equal(t, "foo-dogs", template.Name)

}

func TestServiceUpdateRevisionNameGenerated(t *testing.T) {
orig := newEmptyServiceBetaAPIStyle()

template, err := servinglib.RevisionTemplateOfService(orig)
if err != nil {
t.Fatal(err)
}

template.Name = "foo-asdf"

// Test prefix added by command
action, updated, _, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "--image", "gcr.io/foo/quux:xyzzy", "--namespace", "bar", "--async"}, false)
assert.NilError(t, err)
if !action.Matches("update", "services") {
t.Fatalf("Bad action %v", action)
}

template, err = servinglib.RevisionTemplateOfService(updated)
assert.NilError(t, err)
assert.Assert(t, strings.HasPrefix(template.Name, "foo-"))
assert.Assert(t, !(template.Name == "foo-asdf"))
}

func TestServiceUpdateRevisionNameCleared(t *testing.T) {
orig := newEmptyServiceBetaAPIStyle()

template, err := servinglib.RevisionTemplateOfService(orig)
if err != nil {
t.Fatal(err)
}
template.Name = "foo-asdf"

action, updated, _, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "--image", "gcr.io/foo/quux:xyzzy", "--namespace", "bar", "--revision-name=", "--async"}, false)

assert.NilError(t, err)
if !action.Matches("update", "services") {
t.Fatalf("Bad action %v", action)
}

template, err = servinglib.RevisionTemplateOfService(updated)
assert.NilError(t, err)
assert.Assert(t, cmp.Equal(template.Name, ""))
}

func TestServiceUpdateRevisionNameNoMutationNoChange(t *testing.T) {
orig := newEmptyServiceBetaAPIStyle()

template, err := servinglib.RevisionTemplateOfService(orig)
if err != nil {
t.Fatal(err)
}

template.Name = "foo-asdf"

// Test prefix added by command
action, updated, _, err := fakeServiceUpdate(orig, []string{
"service", "update", "foo", "--namespace", "bar", "--async"}, false)
assert.NilError(t, err)
if !action.Matches("update", "services") {
t.Fatalf("Bad action %v", action)
}

template, err = servinglib.RevisionTemplateOfService(updated)
assert.NilError(t, err)
assert.Equal(t, template.Name, "foo-asdf")
}

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

Expand Down Expand Up @@ -456,6 +549,23 @@ func newEmptyService() *v1alpha1.Service {
}
}

func newEmptyServiceBetaAPIStyle() *v1alpha1.Service {
ret := &v1alpha1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "knative.dev/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
},
Spec: v1alpha1.ServiceSpec{},
}
ret.Spec.Template = &v1alpha1.RevisionTemplateSpec{}
ret.Spec.Template.Spec.Containers = []corev1.Container{{}}
return ret
}

func createMockServiceWithResources(t *testing.T, requestCPU, requestMemory, limitsCPU, limitsMemory string) *v1alpha1.Service {
service := &v1alpha1.Service{
TypeMeta: metav1.TypeMeta{
Expand Down
12 changes: 12 additions & 0 deletions pkg/serving/config_changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package serving

import (
"context"
"errors"
"fmt"
"strconv"

Expand Down Expand Up @@ -92,6 +93,17 @@ func UpdateAnnotation(template *servingv1alpha1.RevisionTemplateSpec, annotation
return nil
}

var ApiTooOldError = errors.New("the service is using too old of an API format for the operation")

// UpdateName updates the revision name.
func UpdateName(template *servingv1alpha1.RevisionTemplateSpec, name string) error {
if template.Spec.DeprecatedContainer != nil {
return ApiTooOldError
}
template.Name = name
return nil
}

// EnvToMap is an utility function to translate between the API list form of env vars, and the
// more convenient map form.
func EnvToMap(vars []corev1.EnvVar) (map[string]string, error) {
Expand Down
7 changes: 7 additions & 0 deletions pkg/serving/config_changes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ func TestUpdateContainerPort(t *testing.T) {
checkPortUpdate(t, template, 80)
}

func TestUpdateName(t *testing.T) {
template, _ := getV1alpha1Config()
err := UpdateName(template, "foo-asdf")
assert.NilError(t, err)
assert.Equal(t, "foo-asdf", template.Name)
}

func checkPortUpdate(t *testing.T, template *servingv1alpha1.RevisionTemplateSpec, port int32) {
if template.Spec.GetContainer().Ports[0].ContainerPort != port {
t.Error("Failed to update the container port")
Expand Down
Loading