Skip to content

Commit

Permalink
Support kube-uninject for istioctl command (istio#15573)
Browse files Browse the repository at this point in the history
* Support kube-uninject for istioctl command

Signed-off-by: Chun Lin Yang <[email protected]>

* Address review comments

* avoid duplicated method

Signed-off-by: Chun Lin Yang <[email protected]>

* add more cases: handle enable-core-dump container/dnsConfig/annotations

* add sidecar.istio.io/inject:false always

* avoid panics

Signed-off-by: Chun Lin Yang <[email protected]>

* correct the comment for exported method
  • Loading branch information
clyang82 authored and istio-testing committed Jul 28, 2019
1 parent 00a1d12 commit e0f7803
Show file tree
Hide file tree
Showing 29 changed files with 3,092 additions and 4 deletions.
340 changes: 340 additions & 0 deletions istioctl/cmd/kubeuninject.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
// 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 (
"bufio"
"errors"
"fmt"
"io"
"os"
"reflect"
"strings"

"github.com/ghodss/yaml"
"github.com/spf13/cobra"
"go.uber.org/multierr"
"k8s.io/api/batch/v2alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
yamlDecoder "k8s.io/apimachinery/pkg/util/yaml"

"istio.io/istio/pilot/pkg/kube/inject"
"istio.io/pkg/log"
)

const (
proxyContainerName = "istio-proxy"
initContainerName = "istio-init"
enableCoreDumpContainerName = "enable-core-dump"
envoyVolumeName = "istio-envoy"
certVolumeName = "istio-certs"
annotationPolicy = "sidecar.istio.io/inject"
sidecarAnnotationPrefix = "sidecar.istio.io"
)

func validateUninjectFlags() error {
var err error

if uninjectInFilename == "" {
err = multierr.Append(err, errors.New("filename not specified (see --filename or -f)"))
}
return err
}

// extractResourceFile uninjects the istio proxy from the specified
// kubernetes YAML file.
func extractResourceFile(in io.Reader, out io.Writer) error {
reader := yamlDecoder.NewYAMLReader(bufio.NewReaderSize(in, 4096))
for {
raw, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}

obj, err := inject.FromRawToObject(raw)
if err != nil && !runtime.IsNotRegisteredError(err) {
return multierr.Append(fmt.Errorf("cannot parse YAML input"), err)
}

var updated []byte
if err == nil {
outObject, err := extractObject(obj)
if err != nil {
return err
}
if updated, err = yaml.Marshal(outObject); err != nil {
return err
}
} else {
updated = raw // unchanged
}

if _, err = out.Write(updated); err != nil {
return err
}
if _, err = fmt.Fprint(out, "---\n"); err != nil {
return err
}
}
return nil
}

// removeInjectedContainers removes the injected container name - istio-proxy and istio-init
func removeInjectedContainers(containers []corev1.Container, injectedContainerName string) []corev1.Container {
for index, c := range containers {
if c.Name == injectedContainerName {
if index < len(containers)-1 {
containers = append(containers[:index], containers[index+1:]...)
} else {
containers = append(containers[:index])
}
break
}
}
return containers
}

// removeInjectedVolumes removes the injected volumes - istio-envoy and istio-certs
func removeInjectedVolumes(volumes []corev1.Volume, injectedVolume string) []corev1.Volume {

for index, v := range volumes {
if v.Name == injectedVolume {
if index < len(volumes)-1 {
volumes = append(volumes[:index], volumes[index+1:]...)
} else {
volumes = append(volumes[:index])
}
break
}
}
return volumes
}

func removeDNSConfig(podDNSConfig *corev1.PodDNSConfig) {
if podDNSConfig == nil {
return
}

l := len(podDNSConfig.Searches)
index := 0
for index < l {
s := podDNSConfig.Searches[index]
if strings.Contains(s, "global") {
if index < len(podDNSConfig.Searches)-1 {
podDNSConfig.Searches = append(podDNSConfig.Searches[:index],
podDNSConfig.Searches[index+1:]...)
} else {
podDNSConfig.Searches = append(podDNSConfig.Searches[:index])
}
//reset to 0
index = 0
l = len(podDNSConfig.Searches)
} else {
index++
}
}

}

// handleAnnotations removes the injected annotations which contains sidecar.istio.io
// it adds sidecar.istio.io/inject: false
func handleAnnotations(annotations map[string]string) map[string]string {
if annotations == nil {
annotations = make(map[string]string)
}

for key := range annotations {
if strings.Contains(key, sidecarAnnotationPrefix) {
delete(annotations, key)
}
}
// sidecar.istio.io/inject: false to default the auto-injector in case it is present.
annotations[annotationPolicy] = "false"
return annotations
}

// extractObject extras the sidecar injection and return the uninjected object.
func extractObject(in runtime.Object) (interface{}, error) {
out := in.DeepCopyObject()

var metadata *metav1.ObjectMeta
var podSpec *corev1.PodSpec

// Handle Lists
if list, ok := out.(*corev1.List); ok {
result := list

for i, item := range list.Items {
obj, err := inject.FromRawToObject(item.Raw)
if runtime.IsNotRegisteredError(err) {
continue
}
if err != nil {
return nil, err
}

r, err := extractObject(obj)
if err != nil {
return nil, err
}

re := runtime.RawExtension{}
re.Object = r.(runtime.Object)
result.Items[i] = re
}
return result, nil
}

// CronJobs have JobTemplates in them, instead of Templates, so we
// special case them.
switch v := out.(type) {
case *v2alpha1.CronJob:
job := v
metadata = &job.Spec.JobTemplate.ObjectMeta
podSpec = &job.Spec.JobTemplate.Spec.Template.Spec
case *corev1.Pod:
pod := v
metadata = &pod.ObjectMeta
podSpec = &pod.Spec
default:
// `in` is a pointer to an Object. Dereference it.
outValue := reflect.ValueOf(out).Elem()

templateValue := outValue.FieldByName("Spec").FieldByName("Template")

// `Template` is defined as a pointer in some older API
// definitions, e.g. ReplicationController
if templateValue.Kind() == reflect.Ptr {
if templateValue.IsNil() {
return out, fmt.Errorf("spec.template is required value")
}
templateValue = templateValue.Elem()
}
metadata = templateValue.FieldByName("ObjectMeta").Addr().Interface().(*metav1.ObjectMeta)
podSpec = templateValue.FieldByName("Spec").Addr().Interface().(*corev1.PodSpec)
}

metadata.Annotations = handleAnnotations(metadata.Annotations)
//skip uninjection for pods
sidecarInjected := false
for _, c := range podSpec.Containers {
if c.Name == proxyContainerName {
sidecarInjected = true
}
}
if !sidecarInjected {
return out, nil
}

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)

return out, nil
}

var (
uninjectInFilename string
uninjectOutFilename string
)

func uninjectCommand() *cobra.Command {
uninjectCmd := &cobra.Command{
Use: "kube-uninject",
Short: "Uninject Envoy sidecar from Kubernetes pod resources",
Long: `
kube-uninject is used to prevent Istio from adding a sidecar and
also provides the inverse of "istioctl kube-inject -f".
`,
Example: `
# Update resources before applying.
kubectl apply -f <(istioctl experimental kube-uninject -f <resource.yaml>)
# Create a persistent version of the deployment by removing Envoy sidecar.
istioctl experimental kube-uninject -f deployment.yaml -o deployment-uninjected.yaml
# Update an existing deployment.
kubectl get deployment -o yaml | istioctl experimental kube-uninject -f - | kubectl apply -f -
`,
RunE: func(c *cobra.Command, _ []string) (err error) {

if err = validateUninjectFlags(); err != nil {
return err
}
//get the resource content
var reader io.Reader
if uninjectInFilename == "-" {
reader = os.Stdin
} else {
var in *os.File
if in, err = os.Open(uninjectInFilename); err != nil {
log.Errorf("Error: close file from %s, %s", uninjectInFilename, err)
return err
}
reader = in
defer func() {
if errClose := in.Close(); errClose != nil {
log.Errorf("Error: close file from %s, %s", uninjectInFilename, errClose)

// don't overwrite the previous error
if err == nil {
err = errClose
}
}
}()
}

var writer io.Writer
if uninjectOutFilename == "" {
writer = c.OutOrStdout()
} else {
var out *os.File
if out, err = os.Create(uninjectOutFilename); err != nil {
return err
}
writer = out
defer func() {
if errClose := out.Close(); errClose != nil {
log.Errorf("Error: close file from %s, %s", uninjectOutFilename, errClose)

// don't overwrite the previous error
if err == nil {
err = errClose
}
}
}()
}

return extractResourceFile(reader, writer)
},
}

uninjectCmd.PersistentFlags().StringVarP(&uninjectInFilename, "filename", "f",
"", "Input Kubernetes resource filename")
uninjectCmd.PersistentFlags().StringVarP(&uninjectOutFilename, "output", "o",
"", "Modified output Kubernetes resource filename")

return uninjectCmd
}
Loading

0 comments on commit e0f7803

Please sign in to comment.