istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/tag/generate.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package tag
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"net/url"
    22  	"strings"
    23  
    24  	admitv1 "k8s.io/api/admissionregistration/v1"
    25  	"k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/serializer"
    29  	"k8s.io/apimachinery/pkg/runtime/serializer/json"
    30  
    31  	"istio.io/istio/operator/pkg/helm"
    32  	"istio.io/istio/pkg/kube"
    33  	"istio.io/istio/pkg/maps"
    34  )
    35  
    36  const (
    37  	IstioTagLabel       = "istio.io/tag"
    38  	DefaultRevisionName = "default"
    39  
    40  	defaultChart            = "default"
    41  	pilotDiscoveryChart     = "istio-control/istio-discovery"
    42  	revisionTagTemplateName = "revision-tags.yaml"
    43  	vwhTemplateName         = "validatingwebhook.yaml"
    44  
    45  	istioInjectionWebhookSuffix = "sidecar-injector.istio.io"
    46  
    47  	vwhBaseTemplateName = "istiod-default-validator"
    48  
    49  	operatorNamespace = "operator.istio.io"
    50  )
    51  
    52  // tagWebhookConfig holds config needed to render a tag webhook.
    53  type tagWebhookConfig struct {
    54  	Tag            string
    55  	Revision       string
    56  	URL            string
    57  	Path           string
    58  	CABundle       string
    59  	IstioNamespace string
    60  	Labels         map[string]string
    61  	Annotations    map[string]string
    62  	// FailurePolicy records the failure policy to use for the webhook.
    63  	FailurePolicy map[string]*admitv1.FailurePolicyType
    64  }
    65  
    66  // GenerateOptions is the group of options needed to generate a tag webhook.
    67  type GenerateOptions struct {
    68  	// Tag is the name of the revision tag to generate.
    69  	Tag string
    70  	// Revision is the revision to associate the revision tag with.
    71  	Revision string
    72  	// WebhookName is an override for the mutating webhook name.
    73  	WebhookName string
    74  	// ManifestsPath specifies where the manifests to render the mutatingwebhook can be found.
    75  	// TODO(Monkeyanator) once we stop using Helm templating remove this.
    76  	ManifestsPath string
    77  	// Generate determines whether we should just generate the webhooks without applying. This
    78  	// applying is not done here, but we are looser with checks when doing generate.
    79  	Generate bool
    80  	// Overwrite removes analysis checks around existing webhooks.
    81  	Overwrite bool
    82  	// AutoInjectNamespaces controls, if the sidecars should be injected into all namespaces by default.
    83  	AutoInjectNamespaces bool
    84  	// CustomLabels are labels to add to the generated webhook.
    85  	CustomLabels map[string]string
    86  	// UserManaged indicates whether the revision tag is user managed.
    87  	// If true, the revision tag will not be affected by the installer.
    88  	UserManaged bool
    89  }
    90  
    91  // Generate generates the manifests for a revision tag pointed the given revision.
    92  func Generate(ctx context.Context, client kube.Client, opts *GenerateOptions, istioNS string) (string, error) {
    93  	// abort if there exists a revision with the target tag name
    94  	revWebhookCollisions, err := GetWebhooksWithRevision(ctx, client.Kube(), opts.Tag)
    95  	if err != nil {
    96  		return "", err
    97  	}
    98  	if !opts.Generate && !opts.Overwrite &&
    99  		len(revWebhookCollisions) > 0 && opts.Tag != DefaultRevisionName {
   100  		return "", fmt.Errorf("cannot create revision tag %q: found existing control plane revision with same name", opts.Tag)
   101  	}
   102  
   103  	// find canonical revision webhook to base our tag webhook off of
   104  	revWebhooks, err := GetWebhooksWithRevision(ctx, client.Kube(), opts.Revision)
   105  	if err != nil {
   106  		return "", err
   107  	}
   108  	if len(revWebhooks) == 0 {
   109  		return "", fmt.Errorf("cannot modify tag: cannot find MutatingWebhookConfiguration with revision %q", opts.Revision)
   110  	}
   111  	if len(revWebhooks) > 1 {
   112  		return "", fmt.Errorf("cannot modify tag: found multiple canonical webhooks with revision %q", opts.Revision)
   113  	}
   114  
   115  	whs, err := GetWebhooksWithTag(ctx, client.Kube(), opts.Tag)
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  	if len(whs) > 0 && !opts.Overwrite {
   120  		return "", fmt.Errorf("revision tag %q already exists, and --overwrite is false", opts.Tag)
   121  	}
   122  
   123  	tagWhConfig, err := tagWebhookConfigFromCanonicalWebhook(revWebhooks[0], opts.Tag, istioNS)
   124  	if err != nil {
   125  		return "", fmt.Errorf("failed to create tag webhook config: %w", err)
   126  	}
   127  	tagWhYAML, err := generateMutatingWebhook(tagWhConfig, opts)
   128  	if err != nil {
   129  		return "", fmt.Errorf("failed to create tag webhook: %w", err)
   130  	}
   131  
   132  	if opts.Tag == DefaultRevisionName {
   133  		if !opts.Generate {
   134  			// deactivate other istio-injection=enabled injectors if using default revisions.
   135  			err := DeactivateIstioInjectionWebhook(ctx, client.Kube())
   136  			if err != nil {
   137  				return "", fmt.Errorf("failed deactivating existing default revision: %w", err)
   138  			}
   139  		}
   140  
   141  		// TODO(Monkeyanator) should extract the validationURL from revision's validating webhook here. However,
   142  		// to ease complexity when pointing default to revision without per-revision validating webhook,
   143  		// instead grab the endpoint information from the mutating webhook. This is not strictly correct.
   144  		validationWhConfig, err := fixWhConfig(client, tagWhConfig)
   145  		if err != nil {
   146  			return "", fmt.Errorf("failed to create validating webhook config: %w", err)
   147  		}
   148  
   149  		vwhYAML, err := generateValidatingWebhook(validationWhConfig, opts)
   150  		if err != nil {
   151  			return "", fmt.Errorf("failed to create validating webhook: %w", err)
   152  		}
   153  		tagWhYAML = fmt.Sprintf(`%s
   154  %s
   155  %s`, tagWhYAML, helm.YAMLSeparator, vwhYAML)
   156  	}
   157  
   158  	return tagWhYAML, nil
   159  }
   160  
   161  func fixWhConfig(client kube.Client, whConfig *tagWebhookConfig) (*tagWebhookConfig, error) {
   162  	if whConfig.URL != "" {
   163  		webhookURL, err := url.Parse(whConfig.URL)
   164  		if err == nil {
   165  			webhookURL.Path = "/validate"
   166  			whConfig.URL = webhookURL.String()
   167  		}
   168  	}
   169  
   170  	// ValidatingWebhookConfiguration failurePolicy is managed by Istiod, so if currently we already have a webhook in cluster
   171  	// that is set to `Fail` by Istiod, we avoid of setting it back to the default `Ignore`.
   172  	vwh, err := client.Kube().AdmissionregistrationV1().ValidatingWebhookConfigurations().
   173  		Get(context.Background(), vwhBaseTemplateName, metav1.GetOptions{})
   174  	if err != nil && !errors.IsNotFound(err) {
   175  		return nil, err
   176  	}
   177  	if vwh == nil {
   178  		return whConfig, nil
   179  	}
   180  	if whConfig.FailurePolicy == nil {
   181  		whConfig.FailurePolicy = map[string]*admitv1.FailurePolicyType{}
   182  	}
   183  	for _, wh := range vwh.Webhooks {
   184  		if wh.FailurePolicy != nil && *wh.FailurePolicy == admitv1.Fail {
   185  			whConfig.FailurePolicy[wh.Name] = nil
   186  		} else {
   187  			whConfig.FailurePolicy[wh.Name] = wh.FailurePolicy
   188  		}
   189  	}
   190  	return whConfig, nil
   191  }
   192  
   193  // Create applies the given tag manifests.
   194  func Create(client kube.CLIClient, manifests, ns string) error {
   195  	if err := client.ApplyYAMLContents(ns, manifests); err != nil {
   196  		return fmt.Errorf("failed to apply tag manifests to cluster: %v", err)
   197  	}
   198  	return nil
   199  }
   200  
   201  // generateValidatingWebhook renders a validating webhook configuration from the given tagWebhookConfig.
   202  func generateValidatingWebhook(config *tagWebhookConfig, opts *GenerateOptions) (string, error) {
   203  	r := helm.NewHelmRenderer(opts.ManifestsPath, defaultChart, "Pilot", config.IstioNamespace, nil)
   204  
   205  	if err := r.Run(); err != nil {
   206  		return "", fmt.Errorf("failed running Helm renderer: %v", err)
   207  	}
   208  
   209  	values := fmt.Sprintf(`
   210  global:
   211    istioNamespace: %s
   212  revision: %q
   213  base:
   214    validationURL: %s
   215  `, config.IstioNamespace, config.Revision, config.URL)
   216  
   217  	validatingWebhookYAML, err := r.RenderManifestFiltered(values, func(tmplName string) bool {
   218  		return strings.Contains(tmplName, vwhTemplateName)
   219  	})
   220  	if err != nil {
   221  		return "", fmt.Errorf("failed rendering istio-control manifest: %v", err)
   222  	}
   223  
   224  	scheme := runtime.NewScheme()
   225  	codecFactory := serializer.NewCodecFactory(scheme)
   226  	deserializer := codecFactory.UniversalDeserializer()
   227  	serializer := json.NewSerializerWithOptions(
   228  		json.DefaultMetaFactory, nil, nil, json.SerializerOptions{
   229  			Yaml:   true,
   230  			Pretty: true,
   231  			Strict: true,
   232  		})
   233  
   234  	whObject, _, err := deserializer.Decode([]byte(validatingWebhookYAML), nil, &admitv1.ValidatingWebhookConfiguration{})
   235  	if err != nil {
   236  		return "", fmt.Errorf("could not decode generated webhook: %w", err)
   237  	}
   238  	decodedWh := whObject.(*admitv1.ValidatingWebhookConfiguration)
   239  	for i := range decodedWh.Webhooks {
   240  		decodedWh.Webhooks[i].ClientConfig.CABundle = []byte(config.CABundle)
   241  	}
   242  	decodedWh.Labels = generateLabels(decodedWh.Labels, config.Labels, opts.CustomLabels, opts.UserManaged)
   243  	decodedWh.Annotations = maps.MergeCopy(decodedWh.Annotations, config.Annotations)
   244  	for i := range decodedWh.Webhooks {
   245  		if failurePolicy, ok := config.FailurePolicy[decodedWh.Webhooks[i].Name]; ok {
   246  			decodedWh.Webhooks[i].FailurePolicy = failurePolicy
   247  		}
   248  	}
   249  
   250  	whBuf := new(bytes.Buffer)
   251  	if err = serializer.Encode(decodedWh, whBuf); err != nil {
   252  		return "", err
   253  	}
   254  
   255  	return whBuf.String(), nil
   256  }
   257  
   258  func generateLabels(whLabels, curLabels, customLabels map[string]string, userManaged bool) map[string]string {
   259  	whLabels = maps.MergeCopy(whLabels, curLabels)
   260  	whLabels = maps.MergeCopy(whLabels, customLabels)
   261  	if userManaged {
   262  		for label := range whLabels {
   263  			if strings.Contains(label, operatorNamespace) {
   264  				delete(whLabels, label)
   265  			}
   266  		}
   267  	}
   268  	return whLabels
   269  }
   270  
   271  // generateMutatingWebhook renders a mutating webhook configuration from the given tagWebhookConfig.
   272  func generateMutatingWebhook(config *tagWebhookConfig, opts *GenerateOptions) (string, error) {
   273  	r := helm.NewHelmRenderer(opts.ManifestsPath, pilotDiscoveryChart, "Pilot", config.IstioNamespace, nil)
   274  
   275  	if err := r.Run(); err != nil {
   276  		return "", fmt.Errorf("failed running Helm renderer: %v", err)
   277  	}
   278  
   279  	values := fmt.Sprintf(`
   280  revision: %q
   281  revisionTags:
   282    - %s
   283  
   284  sidecarInjectorWebhook:
   285    enableNamespacesByDefault: %t
   286    objectSelector:
   287      enabled: true
   288      autoInject: true
   289  
   290  istiodRemote:
   291    injectionURL: %s
   292  `, config.Revision, config.Tag, opts.AutoInjectNamespaces, config.URL)
   293  
   294  	tagWebhookYaml, err := r.RenderManifestFiltered(values, func(tmplName string) bool {
   295  		return strings.Contains(tmplName, revisionTagTemplateName)
   296  	})
   297  	if err != nil {
   298  		return "", fmt.Errorf("failed rendering istio-control manifest: %v", err)
   299  	}
   300  
   301  	scheme := runtime.NewScheme()
   302  	codecFactory := serializer.NewCodecFactory(scheme)
   303  	deserializer := codecFactory.UniversalDeserializer()
   304  	serializer := json.NewSerializerWithOptions(
   305  		json.DefaultMetaFactory, nil, nil, json.SerializerOptions{
   306  			Yaml:   true,
   307  			Pretty: true,
   308  			Strict: true,
   309  		})
   310  
   311  	whObject, _, err := deserializer.Decode([]byte(tagWebhookYaml), nil, &admitv1.MutatingWebhookConfiguration{})
   312  	if err != nil {
   313  		return "", fmt.Errorf("could not decode generated webhook: %w", err)
   314  	}
   315  	decodedWh := whObject.(*admitv1.MutatingWebhookConfiguration)
   316  	for i := range decodedWh.Webhooks {
   317  		decodedWh.Webhooks[i].ClientConfig.CABundle = []byte(config.CABundle)
   318  		if decodedWh.Webhooks[i].ClientConfig.Service != nil {
   319  			decodedWh.Webhooks[i].ClientConfig.Service.Path = &config.Path
   320  		}
   321  	}
   322  	if opts.WebhookName != "" {
   323  		decodedWh.Name = opts.WebhookName
   324  	}
   325  	decodedWh.Labels = generateLabels(decodedWh.Labels, config.Labels, opts.CustomLabels, opts.UserManaged)
   326  	decodedWh.Annotations = maps.MergeCopy(decodedWh.Annotations, config.Annotations)
   327  	whBuf := new(bytes.Buffer)
   328  	if err = serializer.Encode(decodedWh, whBuf); err != nil {
   329  		return "", err
   330  	}
   331  
   332  	return whBuf.String(), nil
   333  }
   334  
   335  // tagWebhookConfigFromCanonicalWebhook parses configuration needed to create tag webhook from existing revision webhook.
   336  func tagWebhookConfigFromCanonicalWebhook(wh admitv1.MutatingWebhookConfiguration, tagName, istioNS string) (*tagWebhookConfig, error) {
   337  	rev, err := GetWebhookRevision(wh)
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  	// if the revision is "default", render templates with an empty revision
   342  	if rev == DefaultRevisionName {
   343  		rev = ""
   344  	}
   345  
   346  	var injectionURL, caBundle, path string
   347  	found := false
   348  	for _, w := range wh.Webhooks {
   349  		if strings.HasSuffix(w.Name, istioInjectionWebhookSuffix) {
   350  			found = true
   351  			caBundle = string(w.ClientConfig.CABundle)
   352  			if w.ClientConfig.URL != nil {
   353  				injectionURL = *w.ClientConfig.URL
   354  			}
   355  			if w.ClientConfig.Service != nil {
   356  				if w.ClientConfig.Service.Path != nil {
   357  					path = *w.ClientConfig.Service.Path
   358  				}
   359  			}
   360  			break
   361  		}
   362  	}
   363  	if !found {
   364  		return nil, fmt.Errorf("could not find sidecar-injector webhook in canonical webhook %q", wh.Name)
   365  	}
   366  
   367  	// Here we filter out the "app" label, to generate a general label set for the incoming generated
   368  	// MutatingWebhookConfiguration and ValidatingWebhookConfiguration. The app of the webhooks are not general
   369  	// since they are functioned differently with different name.
   370  	// The filtered common labels are then added to the incoming generated
   371  	// webhooks, which aids in managing these webhooks via the istioctl/operator.
   372  	filteredLabels := make(map[string]string)
   373  	for k, v := range wh.Labels {
   374  		if k != "app" {
   375  			filteredLabels[k] = v
   376  		}
   377  	}
   378  
   379  	return &tagWebhookConfig{
   380  		Tag:            tagName,
   381  		Revision:       rev,
   382  		URL:            injectionURL,
   383  		CABundle:       caBundle,
   384  		IstioNamespace: istioNS,
   385  		Path:           path,
   386  		Labels:         filteredLabels,
   387  		Annotations:    wh.Annotations,
   388  		FailurePolicy:  map[string]*admitv1.FailurePolicyType{},
   389  	}, nil
   390  }