Repository: knative/client (go)

Issue:
Add "kn service import"
<!-- If you need to report a security issue with Knative, send an email to knative-security@googlegroups.com. -->
## In what area(s)?

<!-- Remove the '> ' to select -->

<!--
Classifications:
> /kind good-first-issue
> /kind feature
> /kind proposal
> /kind doc
> /kind cleanup
-->

## Describe the feature:

Add a `kn service import` command to import a YAML/JSON description of a service. This is the equivalent of `kubectl create -f`  but can be used also via the Knative API contract. The idea is that `kn service export` (#653) and `kn service import` are complementary.

Possible options:

* `--apply` : Use an apply algorithm like with `kubectl apply` (i.e. add the applied resource as an annotation and perform a three-way diff when actually applying). This would allow also an idempotent application of exported resources, very useful in a CI context where `kn` is used as a last step for deployment/update).
* `--namespace` for specifying the target namespace


Pull request:
feat: Add service import command
## Description

The PR is missing any kind of test coverage, TBD. I'd like to gather early feedback on the implementation approach here. 

## Changes

<!-- Please add list of more detailed changes. These changes should be reflected also in the commit messages -->

* Add `service import` command

## Reference

<!-- Please add the corresponding issue number which this pull request is about to fix -->
Fixes #654 

<!--
Please add an entry 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
/hold
/cc @rhuss @navidshaikh 


Patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index eda9aebe..c561ba9a 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -45,6 +45,10 @@
 | 🎁
 | Add WithLabel list filter to serving client lib
 | https://github.com/knative/client/pull/1054[#1054]
+
+| 🎁
+| Add `kn service import` command (experimental)
+| https://github.com/knative/client/pull/1065[#1065]
 |===
 
 ## v0.18.1 (2020-10-13)
diff --git a/docs/cmd/kn_service.md b/docs/cmd/kn_service.md
index 27ff8f53..5e96c2e0 100644
--- a/docs/cmd/kn_service.md
+++ b/docs/cmd/kn_service.md
@@ -32,6 +32,7 @@ kn service
 * [kn service delete](kn_service_delete.md)	 - Delete services
 * [kn service describe](kn_service_describe.md)	 - Show details of a service
 * [kn service export](kn_service_export.md)	 - Export a service and its revisions
+* [kn service import](kn_service_import.md)	 - Import a service and its revisions (experimental)
 * [kn service list](kn_service_list.md)	 - List services
 * [kn service update](kn_service_update.md)	 - Update a service
 
diff --git a/docs/cmd/kn_service_import.md b/docs/cmd/kn_service_import.md
new file mode 100644
index 00000000..78440eab
--- /dev/null
+++ b/docs/cmd/kn_service_import.md
@@ -0,0 +1,45 @@
+## kn service import
+
+Import a service and its revisions (experimental)
+
+### Synopsis
+
+Import a service and its revisions (experimental)
+
+```
+kn service import FILENAME
+```
+
+### Examples
+
+```
+
+ # Import a service from YAML file
+ kn service import /path/to/file.yaml
+
+ # Import a service from JSON file
+ kn service import /path/to/file.json
+```
+
+### Options
+
+```
+  -h, --help               help for import
+  -n, --namespace string   Specify the namespace to operate in.
+      --no-wait            Do not wait for 'service import' operation to be completed.
+      --wait               Wait for 'service import' operation to be completed. (default true)
+      --wait-timeout int   Seconds to wait before giving up on waiting for service to be ready. (default 600)
+```
+
+### Options inherited from parent commands
+
+```
+      --config string       kn configuration file (default: ~/.config/kn/config.yaml)
+      --kubeconfig string   kubectl configuration file (default: ~/.kube/config)
+      --log-http            log http traffic
+```
+
+### SEE ALSO
+
+* [kn service](kn_service.md)	 - Manage Knative services
+
diff --git a/pkg/kn/commands/service/import.go b/pkg/kn/commands/service/import.go
new file mode 100644
index 00000000..a785714d
--- /dev/null
+++ b/pkg/kn/commands/service/import.go
@@ -0,0 +1,143 @@
+// Copyright © 2020 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 service
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"os"
+
+	"github.com/spf13/cobra"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/yaml"
+	"k8s.io/client-go/util/retry"
+
+	clientv1alpha1 "knative.dev/client/pkg/apis/client/v1alpha1"
+	"knative.dev/client/pkg/kn/commands"
+	clientservingv1 "knative.dev/client/pkg/serving/v1"
+	"knative.dev/pkg/kmeta"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+)
+
+// NewServiceImportCommand returns a new command for importing a service.
+func NewServiceImportCommand(p *commands.KnParams) *cobra.Command {
+	var waitFlags commands.WaitFlags
+
+	command := &cobra.Command{
+		Use:   "import FILENAME",
+		Short: "Import a service and its revisions (experimental)",
+		Example: `
+ # Import a service from YAML file
+ kn service import /path/to/file.yaml
+
+ # Import a service from JSON file
+ kn service import /path/to/file.json`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			if len(args) != 1 {
+				return errors.New("'kn service import' requires filename of import file as single argument")
+			}
+			filename := args[0]
+
+			namespace, err := p.GetNamespace(cmd)
+			if err != nil {
+				return err
+			}
+
+			client, err := p.NewServingClient(namespace)
+			if err != nil {
+				return err
+			}
+
+			return importWithOwnerRef(client, filename, cmd.OutOrStdout(), waitFlags)
+		},
+	}
+	flags := command.Flags()
+	commands.AddNamespaceFlags(flags, false)
+	waitFlags.AddConditionWaitFlags(command, commands.WaitDefaultTimeout, "import", "service", "ready")
+
+	return command
+}
+
+func importWithOwnerRef(client clientservingv1.KnServingClient, filename string, out io.Writer, waitFlags commands.WaitFlags) error {
+	var export clientv1alpha1.Export
+	file, err := os.Open(filename)
+	if err != nil {
+		return err
+	}
+	decoder := yaml.NewYAMLOrJSONDecoder(file, 512)
+	err = decoder.Decode(&export)
+	if err != nil {
+		return err
+	}
+	if export.Spec.Service.Name == "" {
+		return fmt.Errorf("provided import file doesn't contain service name, please note that only kn's custom export format is supported")
+	}
+
+	serviceName := export.Spec.Service.Name
+
+	// Return error if service already exists
+	svcExists, err := serviceExists(client, serviceName)
+	if err != nil {
+		return err
+	}
+	if svcExists {
+		return fmt.Errorf("cannot import service '%s' in namespace '%s' because the service already exists",
+			serviceName, client.Namespace())
+	}
+
+	err = client.CreateService(&export.Spec.Service)
+	if err != nil {
+		return err
+	}
+
+	// Retrieve current Configuration to be use in OwnerReference
+	currentConf, err := getConfigurationWithRetry(client, serviceName)
+	if err != nil {
+		return err
+	}
+
+	// Create revision with current Configuration's OwnerReference
+	if len(export.Spec.Revisions) > 0 {
+		for _, r := range export.Spec.Revisions {
+			tmp := r.DeepCopy()
+			// OwnerRef ensures that Revisions are recognized by controller
+			tmp.OwnerReferences = []metav1.OwnerReference{*kmeta.NewControllerRef(currentConf)}
+			if err = client.CreateRevision(tmp); err != nil {
+				return err
+			}
+		}
+	}
+
+	err = waitIfRequested(client, serviceName, waitFlags, "Importing", "imported", out)
+	if err != nil {
+		return err
+	}
+	return err
+}
+
+func getConfigurationWithRetry(client clientservingv1.KnServingClient, name string) (*servingv1.Configuration, error) {
+	var conf *servingv1.Configuration
+	var err error
+	err = retry.OnError(retry.DefaultBackoff, func(err error) bool {
+		return apierrors.IsNotFound(err)
+	}, func() error {
+		conf, err = client.GetConfiguration(name)
+		return err
+	})
+	return conf, err
+}
diff --git a/pkg/kn/commands/service/service.go b/pkg/kn/commands/service/service.go
index 72c73a04..c8126e55 100644
--- a/pkg/kn/commands/service/service.go
+++ b/pkg/kn/commands/service/service.go
@@ -44,6 +44,7 @@ func NewServiceCommand(p *commands.KnParams) *cobra.Command {
 	serviceCmd.AddCommand(NewServiceUpdateCommand(p))
 	serviceCmd.AddCommand(NewServiceApplyCommand(p))
 	serviceCmd.AddCommand(NewServiceExportCommand(p))
+	serviceCmd.AddCommand(NewServiceImportCommand(p))
 	return serviceCmd
 }
 
diff --git a/pkg/serving/v1/client.go b/pkg/serving/v1/client.go
index aa60f6bd..108a1890 100644
--- a/pkg/serving/v1/client.go
+++ b/pkg/serving/v1/client.go
@@ -97,6 +97,12 @@ type KnServingClient interface {
 	// current template.
 	GetBaseRevision(service *servingv1.Service) (*servingv1.Revision, error)
 
+	// Create revision
+	CreateRevision(revision *servingv1.Revision) error
+
+	// Update revision
+	UpdateRevision(revision *servingv1.Revision) error
+
 	// List revisions
 	ListRevisions(opts ...ListConfig) (*servingv1.RevisionList, error)
 
@@ -423,6 +429,24 @@ func getBaseRevision(cl KnServingClient, service *servingv1.Service) (*servingv1
 	return nil, noBaseRevisionError
 }
 
+// Create a revision
+func (cl *knServingClient) CreateRevision(revision *servingv1.Revision) error {
+	rev, err := cl.client.Revisions(cl.namespace).Create(context.TODO(), revision, v1.CreateOptions{})
+	if err != nil {
+		return clienterrors.GetError(err)
+	}
+	return updateServingGvk(rev)
+}
+
+// Update the given service
+func (cl *knServingClient) UpdateRevision(revision *servingv1.Revision) error {
+	_, err := cl.client.Revisions(cl.namespace).Update(context.TODO(), revision, v1.UpdateOptions{})
+	if err != nil {
+		return err
+	}
+	return updateServingGvk(revision)
+}
+
 // Delete a revision by name
 func (cl *knServingClient) DeleteRevision(name string, timeout time.Duration) error {
 	revision, err := cl.client.Revisions(cl.namespace).Get(context.TODO(), name, v1.GetOptions{})
diff --git a/pkg/serving/v1/client_mock.go b/pkg/serving/v1/client_mock.go
index 2586b269..8d72373e 100644
--- a/pkg/serving/v1/client_mock.go
+++ b/pkg/serving/v1/client_mock.go
@@ -202,6 +202,28 @@ func (c *MockKnServingClient) GetConfiguration(name string) (*servingv1.Configur
 	return call.Result[0].(*servingv1.Configuration), mock.ErrorOrNil(call.Result[1])
 }
 
+// CreateRevision records a call CreateRevision
+func (sr *ServingRecorder) CreateRevision(revision interface{}, err error) {
+	sr.r.Add("CreateRevision", []interface{}{revision}, []interface{}{err})
+}
+
+// CreateRevision creates a new revision
+func (c *MockKnServingClient) CreateRevision(revision *servingv1.Revision) error {
+	call := c.recorder.r.VerifyCall("CreateRevision", revision)
+	return mock.ErrorOrNil(call.Result[0])
+}
+
+// UpdateRevision records a call UpdateRevision
+func (sr *ServingRecorder) UpdateRevision(revision interface{}, err error) {
+	sr.r.Add("UpdateRevision", []interface{}{revision}, []interface{}{err})
+}
+
+// UpdateRevision updates given revision
+func (c *MockKnServingClient) UpdateRevision(revision *servingv1.Revision) error {
+	call := c.recorder.r.VerifyCall("UpdateRevision", revision)
+	return mock.ErrorOrNil(call.Result[0])
+}
+
 // Check that every recorded method has been called
 func (sr *ServingRecorder) Validate() {
 	sr.r.CheckThatAllRecordedMethodsHaveBeenCalled()


Test patch:
diff --git a/pkg/kn/commands/service/import_test.go b/pkg/kn/commands/service/import_test.go
new file mode 100644
index 00000000..684812a2
--- /dev/null
+++ b/pkg/kn/commands/service/import_test.go
@@ -0,0 +1,280 @@
+// Copyright © 2020 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 service
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"gotest.tools/assert"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	knclient "knative.dev/client/pkg/serving/v1"
+	"knative.dev/client/pkg/util"
+	"knative.dev/client/pkg/util/mock"
+	"knative.dev/client/pkg/wait"
+	servingv1 "knative.dev/serving/pkg/apis/serving/v1"
+)
+
+func TestServiceImportFilenameError(t *testing.T) {
+	client := knclient.NewMockKnServiceClient(t)
+	r := client.Recorder()
+
+	_, err := executeServiceCommand(client, "import")
+
+	assert.Assert(t, err != nil)
+	assert.Assert(t, util.ContainsAll(err.Error(), "'kn service import'", "requires", "file", "single", "argument"))
+	assert.Error(t, err, "'kn service import' requires filename of import file as single argument")
+	r.Validate()
+}
+
+func TestServiceImportExistError(t *testing.T) {
+	file, err := generateFile([]byte(exportYAML))
+	assert.NilError(t, err)
+	defer os.RemoveAll(filepath.Dir(file))
+
+	client := knclient.NewMockKnServiceClient(t)
+	r := client.Recorder()
+
+	r.GetService("foo", nil, nil)
+	_, err = executeServiceCommand(client, "import", file)
+
+	assert.Assert(t, err != nil)
+	assert.Assert(t, util.ContainsAll(err.Error(), "'foo'", "default", "service", "already", "exists"))
+	r.Validate()
+}
+
+func TestServiceImport(t *testing.T) {
+	file, err := generateFile([]byte(exportYAML))
+	assert.NilError(t, err)
+	defer os.RemoveAll(filepath.Dir(file))
+
+	client := knclient.NewMockKnServiceClient(t)
+	r := client.Recorder()
+
+	r.GetService("foo", nil, errors.NewNotFound(servingv1.Resource("service"), "foo"))
+	r.CreateService(mock.Any(), nil)
+	r.GetConfiguration("foo", nil, nil)
+	r.WaitForService("foo", mock.Any(), wait.NoopMessageCallback(), nil, time.Second)
+	r.GetService("foo", getServiceWithUrl("foo", "http://foo.example.com"), nil)
+
+	out, err := executeServiceCommand(client, "import", file)
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(out, "Service", "'foo'", "default", "imported"))
+	r.Validate()
+}
+
+func TestServiceImportNoWait(t *testing.T) {
+	file, err := generateFile([]byte(exportYAML))
+	assert.NilError(t, err)
+	defer os.RemoveAll(filepath.Dir(file))
+
+	client := knclient.NewMockKnServiceClient(t)
+	r := client.Recorder()
+
+	r.GetService("foo", nil, errors.NewNotFound(servingv1.Resource("service"), "foo"))
+	r.CreateService(mock.Any(), nil)
+	r.GetConfiguration("foo", nil, nil)
+
+	out, err := executeServiceCommand(client, "import", file, "--no-wait")
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(out, "Service", "imported", "foo"))
+	r.Validate()
+}
+
+func TestServiceImportWitRevisions(t *testing.T) {
+	file, err := generateFile([]byte(exportWithRevisionsYAML))
+	assert.NilError(t, err)
+	defer os.RemoveAll(filepath.Dir(file))
+
+	client := knclient.NewMockKnServiceClient(t)
+	r := client.Recorder()
+
+	r.GetService("foo", nil, errors.NewNotFound(servingv1.Resource("service"), "foo"))
+	r.CreateService(mock.Any(), nil)
+	r.GetConfiguration("foo", getConfiguration("foo"), nil)
+	// 2 previous Revisions to re-create + 1 latest from CreateService
+	r.CreateRevision(mock.Any(), nil)
+	r.CreateRevision(mock.Any(), nil)
+	r.WaitForService("foo", mock.Any(), wait.NoopMessageCallback(), nil, time.Second)
+	r.GetService("foo", getServiceWithUrl("foo", "http://foo.example.com"), nil)
+
+	out, err := executeServiceCommand(client, "import", file)
+	assert.NilError(t, err)
+	assert.Assert(t, util.ContainsAll(out, "Service", "imported", "foo"))
+	r.Validate()
+}
+
+func generateFile(fileContent []byte) (string, error) {
+	tempDir, err := ioutil.TempDir("", "kn-file")
+	if err != nil {
+		return "", err
+	}
+
+	tempFile := filepath.Join(tempDir, "import.yaml")
+	if err = ioutil.WriteFile(tempFile, fileContent, os.FileMode(0666)); err != nil {
+		return "", err
+	}
+	return tempFile, nil
+}
+
+func getConfiguration(name string) *servingv1.Configuration {
+	return &servingv1.Configuration{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: name,
+		},
+	}
+}
+
+var exportYAML = `
+apiVersion: client.knative.dev/v1alpha1
+kind: Export
+metadata:
+  creationTimestamp: null
+spec:
+  revisions: null
+  service:
+    apiVersion: serving.knative.dev/v1
+    kind: Service
+    metadata:
+      name: foo
+    spec:
+      template:
+        metadata:
+        spec:
+          containerConcurrency: 0
+          containers:
+          - env:
+            - name: TARGET
+              value: v1
+            image: gcr.io/foo/bar:baz
+            name: user-container
+            readinessProbe:
+              successThreshold: 1
+              tcpSocket:
+                port: 0
+            resources: {}
+          enableServiceLinks: false
+          timeoutSeconds: 300
+    status: {}
+`
+
+var exportWithRevisionsYAML = `
+apiVersion: client.knative.dev/v1alpha1
+kind: Export
+metadata:
+  creationTimestamp: null
+spec:
+  revisions:
+  - apiVersion: serving.knative.dev/v1
+    kind: Revision
+    metadata:
+      annotations:
+        client.knative.dev/user-image: gcr.io/foo/bar:baz
+        serving.knative.dev/routes: foo
+      creationTimestamp: null
+      labels:
+        serving.knative.dev/configuration: foo
+        serving.knative.dev/configurationGeneration: "1"
+        serving.knative.dev/routingState: active
+        serving.knative.dev/service: foo
+      name: foo-rev-1
+    spec:
+      containerConcurrency: 0
+      containers:
+      - env:
+        - name: TARGET
+          value: v1
+        image: gcr.io/foo/bar:baz
+        name: user-container
+        readinessProbe:
+          successThreshold: 1
+          tcpSocket:
+            port: 0
+        resources: {}
+      enableServiceLinks: false
+      timeoutSeconds: 300
+    status: {}
+  - apiVersion: serving.knative.dev/v1
+    kind: Revision
+    metadata:
+      annotations:
+        client.knative.dev/user-image: gcr.io/foo/bar:baz
+        serving.knative.dev/routes: foo
+      creationTimestamp: null
+      labels:
+        serving.knative.dev/configuration: foo
+        serving.knative.dev/configurationGeneration: "2"
+        serving.knative.dev/routingState: active
+        serving.knative.dev/service: foo
+      name: foo-rev-2
+    spec:
+      containerConcurrency: 0
+      containers:
+      - env:
+        - name: TARGET
+          value: v2
+        image: gcr.io/foo/bar:baz
+        name: user-container
+        readinessProbe:
+          successThreshold: 1
+          tcpSocket:
+            port: 0
+        resources: {}
+      enableServiceLinks: false
+      timeoutSeconds: 300
+    status: {}
+  service:
+    apiVersion: serving.knative.dev/v1
+    kind: Service
+    metadata:
+      creationTimestamp: null
+      name: foo
+    spec:
+      template:
+        metadata:
+          annotations:
+            client.knative.dev/user-image: gcr.io/foo/bar:baz
+          name: foo-rev-3
+        spec:
+          containerConcurrency: 0
+          containers:
+          - env:
+            - name: TARGET
+              value: v3
+            image: gcr.io/foo/bar:baz
+            name: user-container
+            readinessProbe:
+              successThreshold: 1
+              tcpSocket:
+                port: 0
+            resources: {}
+          enableServiceLinks: false
+          timeoutSeconds: 300
+      traffic:
+      - latestRevision: false
+        percent: 25
+        revisionName: foo-rev-1
+      - latestRevision: false
+        percent: 25
+        revisionName: foo-rev-2
+      - latestRevision: false
+        percent: 50
+        revisionName: foo-rev-3
+    status: {}
+`
diff --git a/pkg/serving/v1/client_mock_test.go b/pkg/serving/v1/client_mock_test.go
index 7ec9738a..a44f355c 100644
--- a/pkg/serving/v1/client_mock_test.go
+++ b/pkg/serving/v1/client_mock_test.go
@@ -42,6 +42,8 @@ func TestMockKnClient(t *testing.T) {
 	recorder.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback(), nil, 10*time.Second)
 	recorder.GetRevision("hello", nil, nil)
 	recorder.ListRevisions(mock.Any(), nil, nil)
+	recorder.CreateRevision(&servingv1.Revision{}, nil)
+	recorder.UpdateRevision(&servingv1.Revision{}, nil)
 	recorder.DeleteRevision("hello", time.Duration(10)*time.Second, nil)
 	recorder.GetRoute("hello", nil, nil)
 	recorder.ListRoutes(mock.Any(), nil, nil)
@@ -58,6 +60,8 @@ func TestMockKnClient(t *testing.T) {
 	client.WaitForService("hello", time.Duration(10)*time.Second, wait.NoopMessageCallback())
 	client.GetRevision("hello")
 	client.ListRevisions(WithName("blub"))
+	client.CreateRevision(&servingv1.Revision{})
+	client.UpdateRevision(&servingv1.Revision{})
 	client.DeleteRevision("hello", time.Duration(10)*time.Second)
 	client.GetRoute("hello")
 	client.ListRoutes(WithName("blub"))
diff --git a/test/e2e/service_import_test.go b/test/e2e/service_import_test.go
new file mode 100644
index 00000000..903e91ce
--- /dev/null
+++ b/test/e2e/service_import_test.go
@@ -0,0 +1,94 @@
+// 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 im
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build e2e
+// +build !eventing
+
+package e2e
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"gotest.tools/assert"
+	"knative.dev/client/lib/test"
+	"knative.dev/client/pkg/util"
+)
+
+func TestServiceImport(t *testing.T) {
+	t.Parallel()
+	it, err := test.NewKnTest()
+	assert.NilError(t, err)
+	defer func() {
+		assert.NilError(t, it.Teardown())
+	}()
+
+	r := test.NewKnRunResultCollector(t, it)
+	defer r.DumpIfFailed()
+
+	tempDir, err := ioutil.TempDir("", "kn-file")
+	defer os.RemoveAll(tempDir)
+	assert.NilError(t, err)
+
+	t.Log("import service foo with revision")
+	testFile := filepath.Join(tempDir, "foo-with-revisions")
+
+	serviceCreateWithOptions(r, "foo", "--revision-name", "foo-rev-1")
+	test.ServiceUpdate(r, "foo", "--env", "TARGET=v2", "--revision-name", "foo-rev-2")
+	test.ServiceUpdate(r, "foo", "--traffic", "foo-rev-1=50,foo-rev-2=50")
+	serviceExportToFile(r, "foo", testFile, true)
+	test.ServiceDelete(r, "foo")
+	serviceImport(r, testFile)
+
+	t.Log("import existing service foo error")
+	serviceImportExistsError(r, testFile)
+
+	t.Log("import service from missing file error")
+	serviceImportFileError(r, testFile+"-missing")
+}
+
+func serviceExportToFile(r *test.KnRunResultCollector, serviceName, filename string, withRevisions bool) {
+	command := []string{"service", "export", serviceName, "-o", "yaml", "--mode", "export"}
+	if withRevisions {
+		command = append(command, "--with-revisions")
+	}
+	out := r.KnTest().Kn().Run(command...)
+	r.AssertNoError(out)
+	err := ioutil.WriteFile(filename, []byte(out.Stdout), test.FileModeReadWrite)
+	assert.NilError(r.T(), err)
+}
+
+func serviceImport(r *test.KnRunResultCollector, filename string) {
+	command := []string{"service", "import", filename}
+
+	out := r.KnTest().Kn().Run(command...)
+	r.AssertNoError(out)
+	assert.Check(r.T(), util.ContainsAllIgnoreCase(out.Stdout, "service", "importing", "namespace", r.KnTest().Kn().Namespace(), "ready"))
+}
+
+func serviceImportExistsError(r *test.KnRunResultCollector, filename string) {
+	command := []string{"service", "import", filename}
+
+	out := r.KnTest().Kn().Run(command...)
+	r.AssertError(out)
+	assert.Check(r.T(), util.ContainsAllIgnoreCase(out.Stderr, "service", "already", "exists"))
+}
+
+func serviceImportFileError(r *test.KnRunResultCollector, filePath string) {
+	out := r.KnTest().Kn().Run("service", "import", filePath)
+	r.AssertError(out)
+	assert.Check(r.T(), util.ContainsAllIgnoreCase(out.Stderr, "no", "such", "file", "directory", filePath))
+}