diff --git a/istioctl/cmd/remove-from-mesh.go b/istioctl/cmd/remove-from-mesh.go new file mode 100644 index 000000000000..800ba360bdb3 --- /dev/null +++ b/istioctl/cmd/remove-from-mesh.go @@ -0,0 +1,213 @@ +// Copyright 2019 Istio 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 cmd + +import ( + "fmt" + "io" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + + "istio.io/istio/pkg/config/schemas" + + "github.com/spf13/cobra" + "go.uber.org/multierr" + "k8s.io/client-go/kubernetes" + + "istio.io/istio/istioctl/pkg/util/handlers" + "istio.io/pkg/log" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func removeFromMeshCmd() *cobra.Command { + addToMeshCmd := &cobra.Command{ + Use: "remove-from-mesh", + Aliases: []string{"remove"}, + Short: "Remove workloads from Istio service mesh", + RunE: func(cmd *cobra.Command, args []string) error { + cmd.HelpFunc()(cmd, args) + if len(args) != 0 { + return fmt.Errorf("unknown resource type %q", args[0]) + } + return nil + }, + } + addToMeshCmd.AddCommand(svcUnMeshifyCmd()) + addToMeshCmd.AddCommand(externalSvcUnMeshifyCmd()) + return addToMeshCmd +} + +func svcUnMeshifyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "service", + Short: "Remove Service from Istio service mesh", + Long: `istioctl experimental remove-from-mesh service restarts pods with the Istio sidecar un-injected. +THIS COMMAND IS STILL UNDER ACTIVE DEVELOPMENT AND NOT READY FOR PRODUCTION USE. +`, + Example: `istioctl experimental remove-from-mesh service productpage`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("expecting service name") + } + client, err := interfaceFactory(kubeconfig) + if err != nil { + return err + } + ns := handlers.HandleNamespace(namespace, defaultNamespace) + writer := cmd.OutOrStdout() + _, err = client.CoreV1().Services(ns).Get(args[0], metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("service %q does not exist, skip", args[0]) + } + matchingDeployments, err := findDeploymentsForSvc(client, ns, args[0]) + if err != nil { + return err + } + if len(matchingDeployments) == 0 { + fmt.Fprintf(writer, "No deployments found for service %s.%s\n", args[0], ns) + return nil + } + return unInjectSideCarFromDeployment(client, matchingDeployments, args[0], ns, writer) + }, + } + return cmd +} + +func externalSvcUnMeshifyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "external-service ", + Short: "Remove Service Entry and Kubernetes Service for the external service from Istio service mesh", + Long: `istioctl experimental remove-from-mesh external-service remove the ServiceEntry and\ +the kubernetes Service for the specified external service(eg:services running on VM) from Istio service mesh. +The typical usage scenario is Mesh Expansion on VMs. +THIS COMMAND IS STILL UNDER ACTIVE DEVELOPMENT AND NOT READY FOR PRODUCTION USE. +`, + Example: `istioctl experimental remove-from-mesh external-service vmhttp`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("expecting external service name") + } + client, err := interfaceFactory(kubeconfig) + if err != nil { + return err + } + seClient, err := crdFactory(kubeconfig) + if err != nil { + return err + } + writer := cmd.OutOrStdout() + ns := handlers.HandleNamespace(namespace, defaultNamespace) + _, err = client.CoreV1().Services(ns).Get(args[0], metav1.GetOptions{ + IncludeUninitialized: true}) + if err == nil { + return removeServiceOnVMFromMesh(seClient, client, ns, args[0], writer) + } + return fmt.Errorf("service %q does not exist, skip", args[0]) + }, + } + return cmd +} + +func unInjectSideCarFromDeployment(client kubernetes.Interface, deps []appsv1.Deployment, + svcName, svcNamespace string, writer io.Writer) error { + var errs error + name := strings.Join([]string{svcName, svcNamespace}, ".") + for _, dep := range deps { + log.Debugf("updating deployment %s.%s with Istio sidecar un-injected", + dep.Name, dep.Namespace) + podSpec := dep.Spec.Template.Spec.DeepCopy() + newDep := dep.DeepCopyObject() + depName := strings.Join([]string{dep.Name, dep.Namespace}, ".") + sidecarInjected := false + for _, c := range podSpec.Containers { + if c.Name == proxyContainerName { + sidecarInjected = true + break + } + } + if !sidecarInjected { + fmt.Fprintf(writer, "deployment %q has no Istio sidecar injected. Skip\n", depName) + continue + } + podSpec.InitContainers = removeInjectedContainers(podSpec.InitContainers, initContainerName) + podSpec.InitContainers = removeInjectedContainers(podSpec.InitContainers, enableCoreDumpContainerName) + podSpec.Containers = removeInjectedContainers(podSpec.Containers, proxyContainerName) + podSpec.Volumes = removeInjectedVolumes(podSpec.Volumes, envoyVolumeName) + podSpec.Volumes = removeInjectedVolumes(podSpec.Volumes, certVolumeName) + removeDNSConfig(podSpec.DNSConfig) + res, b := newDep.(*appsv1.Deployment) + if !b { + errs = multierr.Append(fmt.Errorf("failed to update deployment %q for service %q", depName, name), errs) + continue + } + res.Spec.Template.Spec = *podSpec + if _, err := + client.AppsV1().Deployments(svcNamespace).Update(res); err != nil { + errs = multierr.Append(fmt.Errorf("failed to update deployment %q for service %q", depName, name), errs) + continue + + } + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: dep.Name, + Namespace: dep.Namespace, + UID: dep.UID, + }, + } + if _, err := client.AppsV1().Deployments(svcNamespace).UpdateStatus(d); err != nil { + errs = multierr.Append(fmt.Errorf("failed to update deployment %q for service %q", depName, name), errs) + continue + } + fmt.Fprintf(writer, "deployment %q updated successfully with Istio sidecar un-injected.\n", depName) + } + return errs +} + +// removeServiceOnVMFromMesh removes the Service Entry and K8s service for the specified external service +func removeServiceOnVMFromMesh(dynamicClient dynamic.Interface, client kubernetes.Interface, ns string, + svcName string, writer io.Writer) error { + // Pre-check Kubernetes service and service entry does not exist. + _, err := client.CoreV1().Services(ns).Get(svcName, metav1.GetOptions{ + IncludeUninitialized: true, + }) + if err != nil { + return fmt.Errorf("service %q does not exist, skip", svcName) + } + serviceEntryGVR := schema.GroupVersionResource{ + Group: "networking.istio.io", + Version: schemas.ServiceEntry.Version, + Resource: "serviceentries", + } + _, err = dynamicClient.Resource(serviceEntryGVR).Namespace(ns).Get(resourceName(svcName), metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("service entry %q does not exist, skip", resourceName(svcName)) + } + err = client.CoreV1().Services(ns).Delete(svcName, &metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("failed to delete Kubernetes service %q due to %v", svcName, err) + } + name := strings.Join([]string{svcName, ns}, ".") + fmt.Fprintf(writer, "Kubernetes Service %q has been deleted for external service %q\n", name, svcName) + err = dynamicClient.Resource(serviceEntryGVR).Namespace(ns).Delete(resourceName(svcName), &metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("failed to delete service entry %q due to %v", resourceName(svcName), err) + } + fmt.Fprintf(writer, "Service Entry %q has been deleted for external service %q\n", resourceName(svcName), svcName) + return nil +} diff --git a/istioctl/cmd/remove-from-mesh_test.go b/istioctl/cmd/remove-from-mesh_test.go new file mode 100644 index 000000000000..32b4fe550c50 --- /dev/null +++ b/istioctl/cmd/remove-from-mesh_test.go @@ -0,0 +1,198 @@ +// Copyright 2019 Istio 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 cmd + +import ( + "fmt" + "strings" + "testing" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "istio.io/istio/pkg/config/schemas" + + //"istio.io/istio/pilot/pkg/config/kube/crd" + appsv1 "k8s.io/api/apps/v1" + coreV1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var ( + cannedK8sConfig = []runtime.Object{ + &coreV1.ConfigMapList{Items: []coreV1.ConfigMap{}}, + + &appsv1.DeploymentList{Items: []appsv1.Deployment{ + { + ObjectMeta: metaV1.ObjectMeta{ + Name: "details-v1", + Namespace: "default", + Labels: map[string]string{ + "app": "details", + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &one, + Selector: &metaV1.LabelSelector{ + MatchLabels: map[string]string{"app": "details"}, + }, + Template: coreV1.PodTemplateSpec{ + ObjectMeta: metaV1.ObjectMeta{ + Labels: map[string]string{"app": "details"}, + }, + Spec: coreV1.PodSpec{ + Containers: []v1.Container{ + {Name: "details", Image: "docker.io/istio/examples-bookinfo-details-v1:1.15.0"}, + {Name: "istio-proxy", Image: "docker.io/istio/proxyv2:1.2.2"}, + }, + InitContainers: []v1.Container{ + {Name: "istio-init", Image: "docker.io/istio/proxy_init:1.2.2"}, + }, + }, + }, + }, + }, + }}, + &coreV1.ServiceList{Items: []coreV1.Service{ + { + ObjectMeta: metaV1.ObjectMeta{ + Name: "details", + Namespace: "default", + }, + Spec: coreV1.ServiceSpec{ + Ports: []coreV1.ServicePort{ + { + Port: 9080, + Name: "http", + }, + }, + Selector: map[string]string{"app": "details"}, + }, + }, + { + ObjectMeta: metaV1.ObjectMeta{ + Name: "dummyservice", + Namespace: "default", + }, + Spec: coreV1.ServiceSpec{ + Ports: []coreV1.ServicePort{ + { + Port: 9080, + Name: "http", + }, + }, + Selector: map[string]string{"app": "dummy"}, + }, + }, + { + ObjectMeta: metaV1.ObjectMeta{ + Name: "vmtest", + Namespace: "default", + }, + Spec: coreV1.ServiceSpec{ + Ports: []coreV1.ServicePort{ + { + Port: 9999, + Name: "http", + }, + }, + Selector: map[string]string{"app": "vmtest"}, + }, + }, + }}, + } + cannedDynamicConfig = []runtime.Object{ + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "networking.istio.io/" + schemas.ServiceEntry.Version, + "kind": schemas.ServiceEntry.VariableName, + "metadata": map[string]interface{}{ + "namespace": "default", + "name": "mesh-expansion-vmtest", + }, + }, + }, + } +) + +func TestRemoveFromMesh(t *testing.T) { + cases := []testcase{ + { + description: "Invalid command args", + args: strings.Split("experimental remove-from-mesh service", " "), + expectedException: true, + expectedOutput: "Error: expecting service name\n", + }, + { + description: "valid case", + args: strings.Split("experimental remove-from-mesh service details", " "), + expectedException: false, + k8sConfigs: cannedK8sConfig, + expectedOutput: "deployment \"details-v1.default\" updated successfully with Istio sidecar un-injected.\n", + }, + { + description: "service not exists", + args: strings.Split("experimental remove-from-mesh service test", " "), + expectedException: true, + k8sConfigs: cannedK8sConfig, + expectedOutput: "Error: service \"test\" does not exist, skip\n", + }, + { + description: "service without depolyment", + args: strings.Split("experimental remove-from-mesh service dummyservice", " "), + expectedException: false, + k8sConfigs: cannedK8sConfig, + expectedOutput: "No deployments found for service dummyservice.default\n", + }, + { + description: "Invalid command args - missing external service name", + args: strings.Split("experimental remove-from-mesh external-service", " "), + expectedException: true, + expectedOutput: "Error: expecting external service name\n", + }, + { + description: "service does not exist", + args: strings.Split("experimental remove-from-mesh external-service test", " "), + expectedException: true, + k8sConfigs: cannedK8sConfig, + dynamicConfigs: cannedDynamicConfig, + expectedOutput: "Error: service \"test\" does not exist, skip\n", + }, + { + description: "ServiceEntry does not exist", + args: strings.Split("experimental remove-from-mesh external-service dummyservice", " "), + expectedException: true, + k8sConfigs: cannedK8sConfig, + dynamicConfigs: cannedDynamicConfig, + expectedOutput: "Error: service entry \"mesh-expansion-dummyservice\" does not exist, skip\n", + }, + { + description: "valid case - external service", + args: strings.Split("experimental remove-from-mesh external-service vmtest", " "), + expectedException: false, + k8sConfigs: cannedK8sConfig, + dynamicConfigs: cannedDynamicConfig, + expectedOutput: "Kubernetes Service \"vmtest.default\" has been deleted for external service \"vmtest\"\n" + + "Service Entry \"mesh-expansion-vmtest\" has been deleted for external service \"vmtest\"\n", + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("case %d %s", i, c.description), func(t *testing.T) { + verifyAddToMeshOutput(t, c) + }) + } +} diff --git a/istioctl/cmd/root.go b/istioctl/cmd/root.go index b1c072c1da9a..d7bc15b72650 100644 --- a/istioctl/cmd/root.go +++ b/istioctl/cmd/root.go @@ -112,6 +112,7 @@ debug and diagnose their Istio mesh. experimentalCmd.AddCommand(metricsCmd) experimentalCmd.AddCommand(describe()) experimentalCmd.AddCommand(addToMeshCmd()) + experimentalCmd.AddCommand(removeFromMeshCmd()) manifestCmd := mesh.ManifestCmd() hideInheritedFlags(manifestCmd, "namespace", "istioNamespace")