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 }