diff --git a/istioctl/cmd/add-to-mesh.go b/istioctl/cmd/add-to-mesh.go new file mode 100644 index 000000000000..8f19306ae434 --- /dev/null +++ b/istioctl/cmd/add-to-mesh.go @@ -0,0 +1,202 @@ +// 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" + "io/ioutil" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + "go.uber.org/multierr" + "k8s.io/client-go/kubernetes" + + "istio.io/istio/istioctl/pkg/util/handlers" + istiocmd "istio.io/istio/pilot/cmd" + "istio.io/istio/pkg/kube/inject" + "istio.io/pkg/log" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8s_labels "k8s.io/apimachinery/pkg/labels" + + meshconfig "istio.io/api/mesh/v1alpha1" +) + +func addToMeshCmd() *cobra.Command { + addToMeshCmd := &cobra.Command{ + Use: "add-to-mesh", + Aliases: []string{"add"}, + Short: "Add workloads into 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(svcMeshifyCmd()) + addToMeshCmd.PersistentFlags().StringVar(&meshConfigFile, "meshConfigFile", "", + "mesh configuration filename. Takes precedence over --meshConfigMapName if set") + addToMeshCmd.PersistentFlags().StringVar(&injectConfigFile, "injectConfigFile", "", + "injection configuration filename. Cannot be used with --injectConfigMapName") + addToMeshCmd.PersistentFlags().StringVar(&valuesFile, "valuesFile", "", + "injection values configuration filename.") + + addToMeshCmd.PersistentFlags().StringVar(&meshConfigMapName, "meshConfigMapName", defaultMeshConfigMapName, + fmt.Sprintf("ConfigMap name for Istio mesh configuration, key should be %q", configMapKey)) + addToMeshCmd.PersistentFlags().StringVar(&injectConfigMapName, "injectConfigMapName", defaultInjectConfigMapName, + fmt.Sprintf("ConfigMap name for Istio sidecar injection, key should be %q.", injectConfigMapKey)) + + return addToMeshCmd +} + +func svcMeshifyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "service", + Short: "Add Service to Istio service mesh", + Long: `istioctl experimental add-to-mesh restarts pods with the Istio sidecar. Use 'add-to-mesh' +to test deployments for compatibility with Istio. If your service does not function after +using 'add-to-mesh' you must re-deploy it and troubleshoot it for Istio compatibility. +See https://istio.io/docs/setup/kubernetes/additional-setup/requirements/ +THIS COMMAND IS STILL UNDER ACTIVE DEVELOPMENT AND NOT READY FOR PRODUCTION USE. +`, + Example: `istioctl experimental add-to-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 + } + var sidecarTemplate, valuesConfig string + ns := handlers.HandleNamespace(namespace, defaultNamespace) + writer := cmd.OutOrStdout() + + meshConfig, err := setupParameters(&sidecarTemplate, &valuesConfig) + if err != nil { + return err + } + 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 injectSideCarIntoDeployment(client, matchingDeployments, sidecarTemplate, valuesConfig, + args[0], ns, meshConfig, writer) + }, + } + return cmd +} +func setupParameters(sidecarTemplate, valuesConfig *string) (*meshconfig.MeshConfig, error) { + var meshConfig *meshconfig.MeshConfig + var err error + if meshConfigFile != "" { + if meshConfig, err = istiocmd.ReadMeshConfig(meshConfigFile); err != nil { + return nil, err + } + } else { + if meshConfig, err = getMeshConfigFromConfigMap(kubeconfig); err != nil { + return nil, err + } + } + if injectConfigFile != "" { + injectionConfig, err := ioutil.ReadFile(injectConfigFile) // nolint: vetshadow + if err != nil { + return nil, err + } + var injectConfig inject.Config + if err := yaml.Unmarshal(injectionConfig, &injectConfig); err != nil { + return nil, multierr.Append(fmt.Errorf("loading --injectConfigFile"), err) + } + *sidecarTemplate = injectConfig.Template + } else if *sidecarTemplate, err = getInjectConfigFromConfigMap(kubeconfig); err != nil { + return nil, err + } + if valuesFile != "" { + valuesConfigBytes, err := ioutil.ReadFile(valuesFile) // nolint: vetshadow + if err != nil { + return nil, err + } + *valuesConfig = string(valuesConfigBytes) + } else if *valuesConfig, err = getValuesFromConfigMap(kubeconfig); err != nil { + return nil, err + } + return meshConfig, err +} + +func injectSideCarIntoDeployment(client kubernetes.Interface, deps []appsv1.Deployment, sidecarTemplate, valuesConfig, + svcName, svcNamespace string, meshConfig *meshconfig.MeshConfig, writer io.Writer) error { + var errs error + for _, dep := range deps { + log.Debugf("updating deployment %s.%s with Istio sidecar injected", + dep.Name, dep.Namespace) + newDep, err := inject.IntoObject(sidecarTemplate, valuesConfig, meshConfig, &dep) + if err != nil { + errs = multierr.Append(fmt.Errorf("failed to update deployment %s.%s for service %s.%s due to %v", + dep.Name, dep.Namespace, svcName, svcNamespace, err), errs) + continue + } + res, b := newDep.(*appsv1.Deployment) + if !b { + errs = multierr.Append(fmt.Errorf("failed to update deployment %s.%s for service %s.%s", + dep.Name, dep.Namespace, svcName, svcNamespace), errs) + continue + } + if _, err := + client.AppsV1().Deployments(svcNamespace).Update(res); err != nil { + errs = multierr.Append(fmt.Errorf("failed to update deployment %s.%s for service %s.%s due to %v", + dep.Name, dep.Namespace, svcName, svcNamespace, err), errs) + continue + + } + if _, err = client.AppsV1().Deployments(svcNamespace).UpdateStatus(res); err != nil { + errs = multierr.Append(fmt.Errorf("failed to update deployment %s.%s for service %s.%s due to %v", + dep.Name, dep.Namespace, svcName, svcNamespace, err), errs) + continue + } + fmt.Fprintf(writer, "deployment %s.%s updated successfully with Istio sidecar injected.\n"+ + "Next Step: Add related labels to the deployment to align with Istio's requirement: "+ + "https://istio.io/docs/setup/kubernetes/additional-setup/requirements/\n", + dep.Name, dep.Namespace) + } + return errs +} + +func findDeploymentsForSvc(client kubernetes.Interface, ns, name string) ([]appsv1.Deployment, error) { + deps := []appsv1.Deployment{} + svc, err := client.CoreV1().Services(ns).Get(name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + svcSelector := k8s_labels.SelectorFromSet(svc.Spec.Selector) + deployments, err := client.AppsV1().Deployments(ns).List(metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, dep := range deployments.Items { + depLabels := k8s_labels.Set(dep.ObjectMeta.Labels) + if svcSelector.Matches(depLabels) { + deps = append(deps, dep) + } + } + return deps, nil +} diff --git a/istioctl/cmd/add-to-mesh_test.go b/istioctl/cmd/add-to-mesh_test.go new file mode 100644 index 000000000000..0a7aec910ae5 --- /dev/null +++ b/istioctl/cmd/add-to-mesh_test.go @@ -0,0 +1,191 @@ +// 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 ( + "bytes" + "fmt" + + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + + "strings" + "testing" + + 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" + "k8s.io/client-go/kubernetes/fake" +) + +type testcase struct { + description string + expectedException bool + args []string + k8sConfigs []runtime.Object + expectedOutput string +} + +var ( + one = int32(1) + tck8sConfigs = []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"}, + }, + }, + }, + }, + }, + }}, + &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"}, + }, + }, + }}, + } +) + +func TestAddToMesh(t *testing.T) { + cases := []testcase{ + { + description: "Invalid command args", + args: strings.Split("experimental add-to-mesh service", " "), + expectedException: true, + expectedOutput: "Error: expecting service name\n", + }, + { + description: "valid case", + args: strings.Split("experimental add-to-mesh service details --meshConfigFile testdata/mesh-config.yaml"+ + " --injectConfigFile testdata/inject-config.yaml"+ + " --valuesFile testdata/inject-values.yaml", " "), + expectedException: false, + k8sConfigs: tck8sConfigs, + expectedOutput: "deployment details-v1.default updated successfully with Istio sidecar injected.\n" + + "Next Step: Add related labels to the deployment to align with Istio's requirement: " + + "https://istio.io/docs/setup/kubernetes/additional-setup/requirements/\n", + }, + { + description: "service not exists", + args: strings.Split("experimental add-to-mesh service test --meshConfigFile testdata/mesh-config.yaml"+ + " --injectConfigFile testdata/inject-config.yaml"+ + " --valuesFile testdata/inject-values.yaml", " "), + expectedException: true, + k8sConfigs: tck8sConfigs, + expectedOutput: "Error: services \"test\" not found\n", + }, + { + description: "service without depolyment", + args: strings.Split("experimental add-to-mesh service dummyservice --meshConfigFile testdata/mesh-config.yaml"+ + " --injectConfigFile testdata/inject-config.yaml"+ + " --valuesFile testdata/inject-values.yaml", " "), + expectedException: false, + k8sConfigs: tck8sConfigs, + expectedOutput: "No deployments found for service dummyservice.default\n", + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("case %d %s", i, c.description), func(t *testing.T) { + verifyAddToMeshOutput(t, c) + }) + } +} + +func verifyAddToMeshOutput(t *testing.T, c testcase) { + t.Helper() + + interfaceFactory = mockInterfaceFactory(c.k8sConfigs) + var out bytes.Buffer + rootCmd := GetRootCmd(c.args) + rootCmd.SetOutput(&out) + + file = "" // Clear, because we re-use + + fErr := rootCmd.Execute() + output := out.String() + + if c.expectedException { + if fErr == nil { + t.Fatalf("Wanted an exception,"+ + "didn't get one, output was %q", output) + } + } else { + if fErr != nil { + t.Fatalf("Unwanted exception: %v", fErr) + } + } + + if c.expectedOutput != "" && c.expectedOutput != output { + t.Fatalf("Unexpected output for 'istioctl %s'\n got: %q\nwant: %q", strings.Join(c.args, " "), output, c.expectedOutput) + } +} + +func mockInterfaceFactory(k8sConfigs []runtime.Object) func(kubeconfig string) (kubernetes.Interface, error) { + outFactory := func(_ string) (kubernetes.Interface, error) { + client := fake.NewSimpleClientset(k8sConfigs...) + return client, nil + } + + return outFactory +} diff --git a/istioctl/cmd/root.go b/istioctl/cmd/root.go index 37e09480a263..e3d1f3fa33dc 100644 --- a/istioctl/cmd/root.go +++ b/istioctl/cmd/root.go @@ -111,6 +111,7 @@ debug and diagnose their Istio mesh. experimentalCmd.AddCommand(uninjectCommand()) experimentalCmd.AddCommand(metricsCmd) experimentalCmd.AddCommand(describe()) + experimentalCmd.AddCommand(addToMeshCmd()) experimentalCmd.AddCommand(mesh.ManifestCmd()) experimentalCmd.AddCommand(mesh.ProfileCmd()) diff --git a/pkg/kube/inject/inject.go b/pkg/kube/inject/inject.go index 028b82a6ba03..7c2f9ce20ea2 100644 --- a/pkg/kube/inject/inject.go +++ b/pkg/kube/inject/inject.go @@ -670,7 +670,7 @@ func IntoResourceFile(sidecarTemplate string, valuesConfig string, meshconfig *m var updated []byte if err == nil { - outObject, err := intoObject(sidecarTemplate, valuesConfig, meshconfig, obj) // nolint: vetshadow + outObject, err := IntoObject(sidecarTemplate, valuesConfig, meshconfig, obj) // nolint: vetshadow if err != nil { return err } @@ -710,7 +710,8 @@ func FromRawToObject(raw []byte) (runtime.Object, error) { return obj, nil } -func intoObject(sidecarTemplate string, valuesConfig string, meshconfig *meshconfig.MeshConfig, in runtime.Object) (interface{}, error) { +// IntoObject convert the incoming resources into Injected resources +func IntoObject(sidecarTemplate string, valuesConfig string, meshconfig *meshconfig.MeshConfig, in runtime.Object) (interface{}, error) { out := in.DeepCopyObject() var deploymentMetadata *metav1.ObjectMeta @@ -731,7 +732,7 @@ func intoObject(sidecarTemplate string, valuesConfig string, meshconfig *meshcon return nil, err } - r, err := intoObject(sidecarTemplate, valuesConfig, meshconfig, obj) // nolint: vetshadow + r, err := IntoObject(sidecarTemplate, valuesConfig, meshconfig, obj) // nolint: vetshadow if err != nil { return nil, err } @@ -785,6 +786,10 @@ func intoObject(sidecarTemplate string, valuesConfig string, meshconfig *meshcon podSpec = templateValue.FieldByName("Spec").Addr().Interface().(*corev1.PodSpec) } + name := metadata.Name + if name == "" { + name = deploymentMetadata.Name + } // Skip injection when host networking is enabled. The problem is // that the iptable changes are assumed to be within the pod when, // in fact, they are changing the routing at the host level. This @@ -793,7 +798,7 @@ func intoObject(sidecarTemplate string, valuesConfig string, meshconfig *meshcon // additional pod failures. if podSpec.HostNetwork { _, _ = fmt.Fprintf(os.Stderr, "Skipping injection because %q has host networking enabled\n", - metadata.Name) + name) return out, nil } @@ -802,7 +807,7 @@ func intoObject(sidecarTemplate string, valuesConfig string, meshconfig *meshcon for _, c := range podSpec.Containers { if c.Name == ProxyContainerName { _, _ = fmt.Fprintf(os.Stderr, "Skipping injection because %q has injected %q sidecar already\n", - metadata.Name, ProxyContainerName) + name, ProxyContainerName) return out, nil } }