| // Copyright 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 tag |
| |
| import ( |
| "bytes" |
| "context" |
| "fmt" |
| "net/url" |
| "os" |
| "strings" |
| ) |
| |
| import ( |
| admit_v1 "k8s.io/api/admissionregistration/v1" |
| "k8s.io/apimachinery/pkg/runtime" |
| "k8s.io/apimachinery/pkg/runtime/serializer" |
| "k8s.io/apimachinery/pkg/runtime/serializer/json" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/operator/pkg/helm" |
| "github.com/apache/dubbo-go-pixiu/pkg/kube" |
| ) |
| |
| const ( |
| IstioTagLabel = "istio.io/tag" |
| DefaultRevisionName = "dubbo" |
| |
| defaultChart = "default" |
| pilotDiscoveryChart = "istio-control/istio-discovery" |
| revisionTagTemplateName = "revision-tags.yaml" |
| vwhTemplateName = "validatingwebhook.yaml" |
| |
| istioInjectionWebhookSuffix = "sidecar-injector.istio.io" |
| ) |
| |
| // tagWebhookConfig holds config needed to render a tag webhook. |
| type tagWebhookConfig struct { |
| Tag string |
| Revision string |
| URL string |
| Path string |
| CABundle string |
| IstioNamespace string |
| } |
| |
| // GenerateOptions is the group of options needed to generate a tag webhook. |
| type GenerateOptions struct { |
| // Tag is the name of the revision tag to generate. |
| Tag string |
| // Revision is the revision to associate the revision tag with. |
| Revision string |
| // WebhookName is an override for the mutating webhook name. |
| WebhookName string |
| // ManifestsPath specifies where the manifests to render the mutatingwebhook can be found. |
| // TODO(Monkeyanator) once we stop using Helm templating remove this. |
| ManifestsPath string |
| // Generate determines whether we should just generate the webhooks without applying. This |
| // applying is not done here but we are looser with checks when doing generate. |
| Generate bool |
| // Overwrite removes analysis checks around existing webhooks. |
| Overwrite bool |
| // AutoInjectNamespaces controls, if the sidecars should be injected into all namespaces by default. |
| AutoInjectNamespaces bool |
| } |
| |
| // Generate generates the manifests for a revision tag pointed the given revision. |
| func Generate(ctx context.Context, client kube.ExtendedClient, opts *GenerateOptions, istioNS string) (string, error) { |
| // abort if there exists a revision with the target tag name |
| revWebhookCollisions, err := GetWebhooksWithRevision(ctx, client, opts.Tag) |
| if err != nil { |
| return "", err |
| } |
| if !opts.Generate && !opts.Overwrite && |
| len(revWebhookCollisions) > 0 && opts.Tag != DefaultRevisionName { |
| return "", fmt.Errorf("cannot create revision tag %q: found existing control plane revision with same name", opts.Tag) |
| } |
| |
| // find canonical revision webhook to base our tag webhook off of |
| revWebhooks, err := GetWebhooksWithRevision(ctx, client, opts.Revision) |
| if err != nil { |
| return "", err |
| } |
| if len(revWebhooks) == 0 { |
| return "", fmt.Errorf("cannot modify tag: cannot find MutatingWebhookConfiguration with revision %q", opts.Revision) |
| } |
| if len(revWebhooks) > 1 { |
| return "", fmt.Errorf("cannot modify tag: found multiple canonical webhooks with revision %q", opts.Revision) |
| } |
| |
| whs, err := GetWebhooksWithTag(ctx, client, opts.Tag) |
| if err != nil { |
| return "", err |
| } |
| if len(whs) > 0 && !opts.Overwrite { |
| return "", fmt.Errorf("revision tag %q already exists, and --overwrite is false", opts.Tag) |
| } |
| |
| tagWhConfig, err := tagWebhookConfigFromCanonicalWebhook(revWebhooks[0], opts.Tag, istioNS) |
| if err != nil { |
| return "", fmt.Errorf("failed to create tag webhook config: %w", err) |
| } |
| tagWhYAML, err := generateMutatingWebhook(tagWhConfig, opts.WebhookName, opts.ManifestsPath, opts.AutoInjectNamespaces) |
| if err != nil { |
| return "", fmt.Errorf("failed to create tag webhook: %w", err) |
| } |
| |
| if opts.Tag == DefaultRevisionName { |
| if !opts.Generate { |
| // deactivate other istio-injection=enabled injectors if using default revisions. |
| err := DeactivateIstioInjectionWebhook(ctx, client) |
| if err != nil { |
| return "", fmt.Errorf("failed deactivating existing default revision: %w", err) |
| } |
| // delete deprecated validating webhook configuration if it exists. |
| err = DeleteDeprecatedValidator(ctx, client) |
| if err != nil { |
| return "", fmt.Errorf("failed removing deprecated validating webhook: %w", err) |
| } |
| } |
| |
| // TODO(Monkeyanator) should extract the validationURL from revision's validating webhook here. However, |
| // to ease complexity when pointing default to revision without per-revision validating webhook, |
| // instead grab the endpoint information from the mutating webhook. This is not strictly correct. |
| validationWhConfig := fixWhConfig(tagWhConfig) |
| |
| vwhYAML, err := generateValidatingWebhook(validationWhConfig, opts.ManifestsPath) |
| if err != nil { |
| return "", fmt.Errorf("failed to create validating webhook: %w", err) |
| } |
| tagWhYAML = fmt.Sprintf(`%s |
| --- |
| %s`, tagWhYAML, vwhYAML) |
| } |
| |
| return tagWhYAML, nil |
| } |
| |
| func fixWhConfig(whConfig *tagWebhookConfig) *tagWebhookConfig { |
| if whConfig.URL != "" { |
| webhookURL, err := url.Parse(whConfig.URL) |
| if err == nil { |
| webhookURL.Path = "/validate" |
| whConfig.URL = webhookURL.String() |
| } |
| } |
| return whConfig |
| } |
| |
| // Create applies the given tag manifests. |
| func Create(client kube.ExtendedClient, manifests string) error { |
| if err := applyYAML(client, manifests, "dubbo-system"); err != nil { |
| return fmt.Errorf("failed to apply tag manifests to cluster: %v", err) |
| } |
| return nil |
| } |
| |
| // generateValidatingWebhook renders a validating webhook configuration from the given tagWebhookConfig. |
| func generateValidatingWebhook(config *tagWebhookConfig, chartPath string) (string, error) { |
| r := helm.NewHelmRenderer(chartPath, defaultChart, "Pilot", config.IstioNamespace, nil) |
| |
| if err := r.Run(); err != nil { |
| return "", fmt.Errorf("failed running Helm renderer: %v", err) |
| } |
| |
| values := fmt.Sprintf(` |
| revision: %q |
| base: |
| validationURL: %s |
| `, config.Revision, config.URL) |
| |
| validatingWebhookYAML, err := r.RenderManifestFiltered(values, func(tmplName string) bool { |
| return strings.Contains(tmplName, vwhTemplateName) |
| }) |
| if err != nil { |
| return "", fmt.Errorf("failed rendering istio-control manifest: %v", err) |
| } |
| |
| scheme := runtime.NewScheme() |
| codecFactory := serializer.NewCodecFactory(scheme) |
| deserializer := codecFactory.UniversalDeserializer() |
| serializer := json.NewSerializerWithOptions( |
| json.DefaultMetaFactory, nil, nil, json.SerializerOptions{ |
| Yaml: true, |
| Pretty: true, |
| Strict: true, |
| }) |
| |
| whObject, _, err := deserializer.Decode([]byte(validatingWebhookYAML), nil, &admit_v1.ValidatingWebhookConfiguration{}) |
| if err != nil { |
| return "", fmt.Errorf("could not decode generated webhook: %w", err) |
| } |
| decodedWh := whObject.(*admit_v1.ValidatingWebhookConfiguration) |
| for i := range decodedWh.Webhooks { |
| decodedWh.Webhooks[i].ClientConfig.CABundle = []byte(config.CABundle) |
| } |
| |
| whBuf := new(bytes.Buffer) |
| if err = serializer.Encode(decodedWh, whBuf); err != nil { |
| return "", err |
| } |
| |
| return whBuf.String(), nil |
| } |
| |
| // generateMutatingWebhook renders a mutating webhook configuration from the given tagWebhookConfig. |
| func generateMutatingWebhook(config *tagWebhookConfig, webhookName, chartPath string, autoInjectNamespaces bool) (string, error) { |
| r := helm.NewHelmRenderer(chartPath, pilotDiscoveryChart, "Pilot", config.IstioNamespace, nil) |
| |
| if err := r.Run(); err != nil { |
| return "", fmt.Errorf("failed running Helm renderer: %v", err) |
| } |
| |
| values := fmt.Sprintf(` |
| revision: %q |
| revisionTags: |
| - %s |
| |
| sidecarInjectorWebhook: |
| enableNamespacesByDefault: %t |
| objectSelector: |
| enabled: true |
| autoInject: true |
| |
| istiodRemote: |
| injectionURL: %s |
| `, config.Revision, config.Tag, autoInjectNamespaces, config.URL) |
| |
| tagWebhookYaml, err := r.RenderManifestFiltered(values, func(tmplName string) bool { |
| return strings.Contains(tmplName, revisionTagTemplateName) |
| }) |
| if err != nil { |
| return "", fmt.Errorf("failed rendering istio-control manifest: %v", err) |
| } |
| |
| scheme := runtime.NewScheme() |
| codecFactory := serializer.NewCodecFactory(scheme) |
| deserializer := codecFactory.UniversalDeserializer() |
| serializer := json.NewSerializerWithOptions( |
| json.DefaultMetaFactory, nil, nil, json.SerializerOptions{ |
| Yaml: true, |
| Pretty: true, |
| Strict: true, |
| }) |
| |
| whObject, _, err := deserializer.Decode([]byte(tagWebhookYaml), nil, &admit_v1.MutatingWebhookConfiguration{}) |
| if err != nil { |
| return "", fmt.Errorf("could not decode generated webhook: %w", err) |
| } |
| decodedWh := whObject.(*admit_v1.MutatingWebhookConfiguration) |
| for i := range decodedWh.Webhooks { |
| decodedWh.Webhooks[i].ClientConfig.CABundle = []byte(config.CABundle) |
| if decodedWh.Webhooks[i].ClientConfig.Service != nil { |
| decodedWh.Webhooks[i].ClientConfig.Service.Path = &config.Path |
| } |
| } |
| if webhookName != "" { |
| decodedWh.Name = webhookName |
| } |
| |
| whBuf := new(bytes.Buffer) |
| if err = serializer.Encode(decodedWh, whBuf); err != nil { |
| return "", err |
| } |
| |
| return whBuf.String(), nil |
| } |
| |
| // tagWebhookConfigFromCanonicalWebhook parses configuration needed to create tag webhook from existing revision webhook. |
| func tagWebhookConfigFromCanonicalWebhook(wh admit_v1.MutatingWebhookConfiguration, tagName, istioNS string) (*tagWebhookConfig, error) { |
| rev, err := GetWebhookRevision(wh) |
| if err != nil { |
| return nil, err |
| } |
| // if the revision is "default", render templates with an empty revision |
| if rev == DefaultRevisionName { |
| rev = "" |
| } |
| |
| var injectionURL, caBundle, path string |
| found := false |
| for _, w := range wh.Webhooks { |
| if strings.HasSuffix(w.Name, istioInjectionWebhookSuffix) { |
| found = true |
| caBundle = string(w.ClientConfig.CABundle) |
| if w.ClientConfig.URL != nil { |
| injectionURL = *w.ClientConfig.URL |
| } |
| if w.ClientConfig.Service != nil { |
| if w.ClientConfig.Service.Path != nil { |
| path = *w.ClientConfig.Service.Path |
| } |
| } |
| break |
| } |
| } |
| if !found { |
| return nil, fmt.Errorf("could not find sidecar-injector webhook in canonical webhook %q", wh.Name) |
| } |
| |
| return &tagWebhookConfig{ |
| Tag: tagName, |
| Revision: rev, |
| URL: injectionURL, |
| CABundle: caBundle, |
| IstioNamespace: istioNS, |
| Path: path, |
| }, nil |
| } |
| |
| // applyYAML taken from remote_secret.go |
| func applyYAML(client kube.ExtendedClient, yamlContent, ns string) error { |
| yamlFile, err := writeToTempFile(yamlContent) |
| if err != nil { |
| return fmt.Errorf("failed creating manifest file: %w", err) |
| } |
| |
| // Apply the YAML to the cluster. |
| if err := client.ApplyYAMLFiles(ns, yamlFile); err != nil { |
| return fmt.Errorf("failed applying manifest %s: %v", yamlFile, err) |
| } |
| return nil |
| } |
| |
| // writeToTempFile taken from remote_secret.go |
| func writeToTempFile(content string) (string, error) { |
| outFile, err := os.CreateTemp("", "revision-tag-manifest-*") |
| if err != nil { |
| return "", fmt.Errorf("failed creating temp file for manifest: %w", err) |
| } |
| defer func() { _ = outFile.Close() }() |
| |
| if _, err := outFile.Write([]byte(content)); err != nil { |
| return "", fmt.Errorf("failed writing manifest file: %w", err) |
| } |
| return outFile.Name(), nil |
| } |