diff --git a/docs/cmd/kn_service_create.md b/docs/cmd/kn_service_create.md index ec259edaf..0ffb240b2 100644 --- a/docs/cmd/kn_service_create.md +++ b/docs/cmd/kn_service_create.md @@ -87,6 +87,7 @@ kn service create NAME --image IMAGE --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. + --scale-init int Initial number of replicas with which a service starts. Can be 0 or a positive integer. --scale-max int Maximum number of replicas. --scale-min int Minimum 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. diff --git a/docs/cmd/kn_service_update.md b/docs/cmd/kn_service_update.md index b4b30654e..007a02923 100644 --- a/docs/cmd/kn_service_update.md +++ b/docs/cmd/kn_service_update.md @@ -70,6 +70,7 @@ kn service update NAME --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. + --scale-init int Initial number of replicas with which a service starts. Can be 0 or a positive integer. --scale-max int Maximum number of replicas. --scale-min int Minimum 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. diff --git a/pkg/kn/commands/service/configuration_edit_flags.go b/pkg/kn/commands/service/configuration_edit_flags.go index 7e2527327..d76d2381f 100644 --- a/pkg/kn/commands/service/configuration_edit_flags.go +++ b/pkg/kn/commands/service/configuration_edit_flags.go @@ -16,6 +16,7 @@ package service import ( "fmt" + "strconv" "strings" "github.com/spf13/cobra" @@ -27,6 +28,7 @@ import ( knflags "knative.dev/client/pkg/kn/flags" servinglib "knative.dev/client/pkg/serving" "knative.dev/client/pkg/util" + "knative.dev/serving/pkg/apis/autoscaling" "knative.dev/serving/pkg/apis/serving" servingv1 "knative.dev/serving/pkg/apis/serving/v1" ) @@ -49,6 +51,7 @@ type ConfigurationEditFlags struct { RevisionName string Annotations []string ClusterLocal bool + ScaleInit int // Preferences about how to do the action. LockToDigest bool @@ -149,6 +152,9 @@ func (p *ConfigurationEditFlags) addSharedFlags(command *cobra.Command) { "any number of times to set multiple annotations. "+ "To unset, specify the annotation name followed by a \"-\" (e.g., name-).") p.markFlagMakesRevision("annotation") + + command.Flags().IntVar(&p.ScaleInit, "scale-init", 0, "Initial number of replicas with which a service starts. Can be 0 or a positive integer.") + p.markFlagMakesRevision("scale-init") } // AddUpdateFlags adds the flags specific to update. @@ -427,6 +433,29 @@ func (p *ConfigurationEditFlags) Apply( servinglib.UpdateUser(template, p.PodSpecFlags.User) } + if cmd.Flags().Changed("scale-init") { + containsAnnotation := func(annotationList []string, annotation string) bool { + for _, element := range annotationList { + if strings.Contains(element, annotation) { + return true + } + } + return false + } + + if cmd.Flags().Changed("annotation") && containsAnnotation(p.Annotations, autoscaling.InitialScaleAnnotationKey) { + return fmt.Errorf("only one of the --scale-init or --annotation %s can be specified", autoscaling.InitialScaleAnnotationKey) + } + annotationsMap := map[string]string{ + autoscaling.InitialScaleAnnotationKey: strconv.Itoa(p.ScaleInit), + } + + err = servinglib.UpdateAnnotations(service, template, annotationsMap, []string{}) + if err != nil { + return err + } + } + return nil } diff --git a/pkg/kn/commands/service/create_mock_test.go b/pkg/kn/commands/service/create_mock_test.go index 7ab5133b2..627c86fd6 100644 --- a/pkg/kn/commands/service/create_mock_test.go +++ b/pkg/kn/commands/service/create_mock_test.go @@ -512,6 +512,39 @@ func getService(name string) *servingv1.Service { return service } +func TestServiceCreateWithInitScaleAsOption(t *testing.T) { + client := knclient.NewMockKnServiceClient(t) + + r := client.Recorder() + + // Check for existing service --> no + r.GetService("foo", nil, errors.NewNotFound(servingv1.Resource("service"), "foo")) + // Create service (don't validate given service --> "Any()" arg is allowed) + r.CreateService(mock.Any(), nil) + // Wait for service to become ready + r.WaitForService("foo", mock.Any(), wait.NoopMessageCallback(), nil, time.Second) + // Get for showing the URL + r.GetService("foo", getServiceWithUrl("foo", "http://foo.example.com"), nil) + + output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--scale-init", "0") + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", "foo", "default")) + + r.Validate() +} + +func TestServiceCreateWithBothAnnotationAndInitScaleAsOption(t *testing.T) { + client := knclient.NewMockKnServiceClient(t) + + r := client.Recorder() + + output, err := executeServiceCommand(client, "create", "foo", "--image", "gcr.io/foo/bar:baz", "--annotation", "autoscaling.knative.dev/initialScale=0", "--scale-init", "0") + assert.Assert(t, err != nil) + assert.Assert(t, util.ContainsAll(output, "only one of the", "--scale-init", "--annotation", "autoscaling.knative.dev/initialScale", "can be specified")) + + r.Validate() +} + func getServiceWithUrl(name string, urlName string) *servingv1.Service { service := servingv1.Service{} url, _ := apis.ParseURL(urlName) diff --git a/pkg/kn/commands/service/service_update_mock_test.go b/pkg/kn/commands/service/service_update_mock_test.go index f55d8d871..783ac7c81 100644 --- a/pkg/kn/commands/service/service_update_mock_test.go +++ b/pkg/kn/commands/service/service_update_mock_test.go @@ -1473,3 +1473,50 @@ func TestServiceUpdateUser(t *testing.T) { r.Validate() } + +func TestServiceUpdateInitialScaleMock(t *testing.T) { + client := clientservingv1.NewMockKnServiceClient(t) + svcName := "svc1" + newService := getService(svcName) + template := &newService.Spec.Template + template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz" + newService.ObjectMeta.Annotations = map[string]string{ + "autoscaling.knative.dev/initialScale": "1", + } + template.ObjectMeta.Annotations = map[string]string{ + "autoscaling.knative.dev/initialScale": "1", + clientserving.UserImageAnnotationKey: "gcr.io/foo/bar:baz", + } + + updatedService := getService(svcName) + template = &updatedService.Spec.Template + template.Spec.Containers[0].Image = "gcr.io/foo/bar:baz" + updatedService.ObjectMeta.Annotations = map[string]string{ + "autoscaling.knative.dev/initialScale": "2", + } + template.ObjectMeta.Annotations = map[string]string{ + "autoscaling.knative.dev/initialScale": "2", + clientserving.UserImageAnnotationKey: "gcr.io/foo/bar:baz", + } + + r := client.Recorder() + recordServiceUpdateWithSuccess(r, svcName, newService, updatedService) + + output, err := executeServiceCommand(client, + "create", svcName, "--image", "gcr.io/foo/bar:baz", + "--scale-init", "1", + "--no-wait", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "created", svcName, "default")) + + output, err = executeServiceCommand(client, + "update", svcName, + "--scale-init", "2", + "--no-wait", "--revision-name=", + ) + assert.NilError(t, err) + assert.Assert(t, util.ContainsAll(output, "updated", svcName, "default")) + + r.Validate() +} diff --git a/test/e2e/service_options_test.go b/test/e2e/service_options_test.go index 741414a08..586eb4304 100644 --- a/test/e2e/service_options_test.go +++ b/test/e2e/service_options_test.go @@ -147,6 +147,15 @@ func TestServiceOptions(t *testing.T) { serviceCreateWithOptions(r, "svc9", "--image", pkgtest.ImagePath("grpc-ping"), "--port", "h2c:8080") validatePort(r, "svc9", 8080, "h2c") test.ServiceDelete(r, "svc9") + + t.Log("create and validate service with scale init option") + serviceCreateWithOptions(r, "svc10", "--scale-init", "1") + validateServiceInitScale(r, "svc10", "1") + test.ServiceUpdate(r, "svc10", "--scale-init", "2") + validateServiceInitScale(r, "svc10", "2") + t.Log("delete service") + test.ServiceDelete(r, "svc10") + } func serviceCreateWithOptions(r *test.KnRunResultCollector, serviceName string, options ...string) { @@ -211,6 +220,13 @@ func validateServiceMaxScale(r *test.KnRunResultCollector, serviceName, maxScale r.AssertNoError(out) } +func validateServiceInitScale(r *test.KnRunResultCollector, serviceName, initScale string) { + jsonpath := "jsonpath={.items[0].spec.template.metadata.annotations.autoscaling\\.knative\\.dev/initialScale}" + out := r.KnTest().Kn().Run("service", "list", serviceName, "-o", jsonpath) + assert.Equal(r.T(), out.Stdout, initScale) + r.AssertNoError(out) +} + func validateServiceAnnotations(r *test.KnRunResultCollector, serviceName string, annotations map[string]string) { metadataAnnotationsJsonpathFormat := "jsonpath={.metadata.annotations.%s}" templateAnnotationsJsonpathFormat := "jsonpath={.spec.template.metadata.annotations.%s}"