diff --git a/docs/cmd/kn_service_create.md b/docs/cmd/kn_service_create.md index 33d8d71a9..e358c7c03 100644 --- a/docs/cmd/kn_service_create.md +++ b/docs/cmd/kn_service_create.md @@ -88,6 +88,7 @@ kn service create NAME --image IMAGE --requests-cpu string DEPRECATED: please use --request instead. The requested CPU (e.g., 250m). --requests-memory string DEPRECATED: please use --request instead. 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}}") + --scale int Minimum and maximum number of replicas. --service-account string Service account name to set. An empty argument ("") clears the service account. The referenced service account must exist in the service's namespace. --user int The user ID to run the container (e.g., 1001). --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-. diff --git a/docs/cmd/kn_service_update.md b/docs/cmd/kn_service_update.md index 87e4c80bb..f8a1dd64b 100644 --- a/docs/cmd/kn_service_update.md +++ b/docs/cmd/kn_service_update.md @@ -71,6 +71,7 @@ kn service update NAME --requests-cpu string DEPRECATED: please use --request instead. The requested CPU (e.g., 250m). --requests-memory string DEPRECATED: please use --request instead. 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}}") + --scale int Minimum and maximum number of replicas. --service-account string Service account name to set. An empty argument ("") clears the service account. The referenced service account must exist in the service's namespace. --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%. diff --git a/pkg/kn/commands/service/configuration_edit_flags.go b/pkg/kn/commands/service/configuration_edit_flags.go index 569ccee2a..6321e9a8a 100644 --- a/pkg/kn/commands/service/configuration_edit_flags.go +++ b/pkg/kn/commands/service/configuration_edit_flags.go @@ -45,6 +45,7 @@ type ConfigurationEditFlags struct { RequestsFlags, LimitsFlags ResourceFlags // TODO: Flag marked deprecated in release v0.15.0, remove in release v0.18.0 Resources knflags.ResourceOptions + Scale int MinScale int MaxScale int ConcurrencyTarget int @@ -187,6 +188,9 @@ func (p *ConfigurationEditFlags) addSharedFlags(command *cobra.Command) { command.Flags().IntVar(&p.MaxScale, "max-scale", 0, "Maximal number of replicas.") p.markFlagMakesRevision("max-scale") + command.Flags().IntVar(&p.Scale, "scale", 0, "Minimum and maximum number of replicas.") + p.markFlagMakesRevision("scale") + command.Flags().StringVar(&p.AutoscaleWindow, "autoscale-window", "", "Duration to look back for making auto-scaling decisions. The service is scaled to zero if no request was received in during that time. (eg: 10s)") p.markFlagMakesRevision("autoscale-window") @@ -437,6 +441,23 @@ func (p *ConfigurationEditFlags) Apply( } } + if cmd.Flags().Changed("scale") { + if cmd.Flags().Changed("max-scale") { + return fmt.Errorf("only --scale or --max-scale can be specified") + } else if cmd.Flags().Changed("min-scale") { + return fmt.Errorf("only --scale or --min-scale can be specified") + } else { + err = servinglib.UpdateMaxScale(template, p.Scale) + if err != nil { + return err + } + err = servinglib.UpdateMinScale(template, p.Scale) + if err != nil { + return err + } + } + } + if cmd.Flags().Changed("autoscale-window") { err = servinglib.UpdateAutoscaleWindow(template, p.AutoscaleWindow) if err != nil { diff --git a/pkg/kn/commands/service/create_test.go b/pkg/kn/commands/service/create_test.go index 82a36e7be..575d38b0c 100644 --- a/pkg/kn/commands/service/create_test.go +++ b/pkg/kn/commands/service/create_test.go @@ -547,6 +547,76 @@ func TestServiceCreateMaxMinScale(t *testing.T) { } } +func TestServiceCreateScale(t *testing.T) { + action, created, _, err := fakeServiceCreate([]string{ + "service", "create", "foo", "--image", "gcr.io/foo/bar:baz", + "--scale", "5", "--no-wait"}, false) + + if err != nil { + t.Fatal(err) + } else if !action.Matches("create", "services") { + t.Fatalf("Bad action %v", action) + } + + template := &created.Spec.Template + + actualAnnos := template.Annotations + expectedAnnos := []string{ + "autoscaling.knative.dev/minScale", "5", + "autoscaling.knative.dev/maxScale", "5", + } + + for i := 0; i < len(expectedAnnos); i += 2 { + anno := expectedAnnos[i] + if actualAnnos[anno] != expectedAnnos[i+1] { + t.Fatalf("Unexpected annotation value for %s : %s (actual) != %s (expected)", + anno, actualAnnos[anno], expectedAnnos[i+1]) + } + } +} + +func TestServiceCreateScaleWithNegativeValue(t *testing.T) { + _, _, _, err := fakeServiceCreate([]string{ + "service", "create", "foo", "--image", "gcr.io/foo/bar:baz", + "--scale", "-1", "--no-wait"}, true) + if err == nil { + t.Fatal(err) + } + expectedErrMsg := "expected 0 <= -1 <= 2147483647: autoscaling.knative.dev/maxScale" + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf("Invalid error output, expected: %s, got : '%s'", expectedErrMsg, err) + } + +} + +func TestServiceCreateScaleWithMaxScaleSet(t *testing.T) { + _, _, _, err := fakeServiceCreate([]string{ + "service", "create", "foo", "--image", "gcr.io/foo/bar:baz", + "--scale", "5", "--max-scale", "2", "--no-wait"}, true) + if err == nil { + t.Fatal(err) + } + expectedErrMsg := "only --scale or --max-scale can be specified" + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf("Invalid error output, expected: %s, got : '%s'", expectedErrMsg, err) + } + +} + +func TestServiceCreateScaleWithMinScaleSet(t *testing.T) { + _, _, _, err := fakeServiceCreate([]string{ + "service", "create", "foo", "--image", "gcr.io/foo/bar:baz", + "--scale", "5", "--min-scale", "2", "--no-wait"}, true) + if err == nil { + t.Fatal(err) + } + expectedErrMsg := "only --scale or --min-scale can be specified" + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf("Invalid error output, expected: %s, got : '%s'", expectedErrMsg, err) + } + +} + func TestServiceCreateRequestsLimitsCPUMemory(t *testing.T) { action, created, _, err := fakeServiceCreate([]string{ "service", "create", "foo", "--image", "gcr.io/foo/bar:baz", diff --git a/pkg/kn/commands/service/update_test.go b/pkg/kn/commands/service/update_test.go index 832fb0ee7..f02c21f1c 100644 --- a/pkg/kn/commands/service/update_test.go +++ b/pkg/kn/commands/service/update_test.go @@ -355,6 +355,96 @@ func TestServiceUpdateMaxMinScale(t *testing.T) { } +func TestServiceUpdateScale(t *testing.T) { + original := newEmptyService() + + action, updated, _, err := fakeServiceUpdate(original, []string{ + "service", "update", "foo", + "--scale", "5", "--no-wait"}) + + if err != nil { + t.Fatal(err) + } else if !action.Matches("update", "services") { + t.Fatalf("Bad action %v", action) + } + + template := updated.Spec.Template + if err != nil { + t.Fatal(err) + } + + actualAnnos := template.Annotations + expectedAnnos := []string{ + "autoscaling.knative.dev/minScale", "5", + "autoscaling.knative.dev/maxScale", "5", + } + + for i := 0; i < len(expectedAnnos); i += 2 { + anno := expectedAnnos[i] + if actualAnnos[anno] != expectedAnnos[i+1] { + t.Fatalf("Unexpected annotation value for %s : %s (actual) != %s (expected)", + anno, actualAnnos[anno], expectedAnnos[i+1]) + } + } + +} + +func TestServiceUpdateScaleWithNegativeValue(t *testing.T) { + original := newEmptyService() + + _, _, _, err := fakeServiceUpdate(original, []string{ + "service", "update", "foo", + "--scale", "-1", "--no-wait"}) + + if err == nil { + t.Fatal("Expected error, got nil") + } + + expectedErrMsg := "expected 0 <= -1 <= 2147483647: autoscaling.knative.dev/maxScale" + + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf("Invalid error output, expected: %s, got : '%s'", expectedErrMsg, err) + } + +} + +func TestServiceUpdateScaleWithMaxScaleSet(t *testing.T) { + original := newEmptyService() + + _, _, _, err := fakeServiceUpdate(original, []string{ + "service", "update", "foo", + "--scale", "5", "--max-scale", "2", "--no-wait"}) + + if err == nil { + t.Fatal("Expected error, got nil") + } + + expectedErrMsg := "only --scale or --max-scale can be specified" + + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf("Invalid error output, expected: %s, got : '%s'", expectedErrMsg, err) + } + +} + +func TestServiceUpdateScaleWithMinScaleSet(t *testing.T) { + original := newEmptyService() + + _, _, _, err := fakeServiceUpdate(original, []string{ + "service", "update", "foo", + "--scale", "5", "--min-scale", "2", "--no-wait"}) + + if err == nil { + t.Fatal("Expected error, got nil") + } + + expectedErrMsg := "only --scale or --min-scale can be specified" + + if !strings.Contains(err.Error(), expectedErrMsg) { + t.Errorf("Invalid error output, expected: %s, got : '%s'", expectedErrMsg, err) + } + +} func TestServiceUpdateEnv(t *testing.T) { orig := newEmptyService() diff --git a/test/e2e/service_options_test.go b/test/e2e/service_options_test.go index 71164bb9e..6d071a5a4 100644 --- a/test/e2e/service_options_test.go +++ b/test/e2e/service_options_test.go @@ -67,7 +67,7 @@ func TestServiceOptions(t *testing.T) { t.Log("delete service") test.ServiceDelete(r, "svc1") - t.Log("create and validate service with min/max scale options ") + t.Log("create and validate service with min/max scale options") serviceCreateWithOptions(r, "svc2", "--min-scale", "1", "--max-scale", "3") validateServiceMinScale(r, "svc2", "1") validateServiceMaxScale(r, "svc2", "3") @@ -76,6 +76,16 @@ func TestServiceOptions(t *testing.T) { test.ServiceUpdate(r, "svc2", "--max-scale", "2") validateServiceMaxScale(r, "svc2", "2") + t.Log("create and validate service with scale options") + serviceCreateWithOptions(r, "svc2a", "--scale", "5") + validateServiceMinScale(r, "svc2a", "5") + validateServiceMaxScale(r, "svc2a", "5") + + t.Log("update and validate service with scale option") + test.ServiceUpdate(r, "svc2a", "--scale", "2") + validateServiceMaxScale(r, "svc2a", "2") + validateServiceMinScale(r, "svc2a", "2") + t.Log("delete service") test.ServiceDelete(r, "svc2")