Skip to content

Commit

Permalink
istioctl add-to-mesh service (istio#16156)
Browse files Browse the repository at this point in the history
* rebase:add k8s service into mesh

* rebase

* rebase

* merge fix
  • Loading branch information
irisdingbj authored and istio-testing committed Aug 22, 2019
1 parent e10ed6e commit 9a4e977
Show file tree
Hide file tree
Showing 4 changed files with 404 additions and 5 deletions.
202 changes: 202 additions & 0 deletions istioctl/cmd/add-to-mesh.go
Original file line number Diff line number Diff line change
@@ -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
}
191 changes: 191 additions & 0 deletions istioctl/cmd/add-to-mesh_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 9a4e977

Please sign in to comment.