Repository: knative/client (go)

Issue:
Add support for --scale-init
Now that we have support for defining the initial scale for a service, it would be nice if `kn` supported it via a top-level option instead of just as an annotation.

e.g.
kn service create echo --image duglin/echo --annotation autoscaling.knative.dev/initialScale=0
vs
kn service create echo --image duglin/echo --scale-init=0

or --scale-initial

Pull request description:
Add changes for --scale-init support
## Description

Now that we have support for defining the initial scale for a service, implementing `--scale-init` with kn as a top-level option instead of just as an annotation.

<!-- Please add a short summary about what the pull request is going to bring on the table -->

## Changes

<!-- Please add list of more detailed changes. These changes should be reflected also in the commit messages -->

* Add support for --scale-init option

## Reference

<!-- Please add the corresponding issue number which this pull request is about to fix -->
Fixes #959 

<!--
Please add an entrty to CHANGELOG.adoc file, too, as part of your Pull Request.

In the following cases, add a short description of PR to the unreleased section in CHANGELOG.adoc:

- 🎁 New feature
- 🐛 Bug fix
- ✨ Feature Update
- 🐣 Refactoring
- 🗑️ Remove feature or internal logic

See other entries in CHANGELOG.adoc as an example for how to add the entry, including a reference to the corresponding issue/PR

PLEASE DON'T ADD THAT LINE HERE IN THE PULL-REQUEST DESCRIPTION BUT DIRECTLY IN CHANGELOG.ADOC AND ADD CHANGELOG.ADOC AS PART OF YOUR PULL-REQUEST.
-->

<!--
To automatically lint go code in this pull request uncomment the line below. You get feedback as comments on your pull request then -->

<!--
/lint
-->


Patch:
diff --git a/docs/cmd/kn_service_create.md b/docs/cmd/kn_service_create.md
index ec259eda..0ffb240b 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 b4b30654..007a0292 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 7e252732..d76d2381 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
 }
 


Test patch:
diff --git a/pkg/kn/commands/service/create_mock_test.go b/pkg/kn/commands/service/create_mock_test.go
index 7ab5133b..627c86fd 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 f55d8d87..783ac7c8 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 741414a0..586eb430 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}"