github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/webhooks/istio_defaulter.go (about) 1 // Copyright (c) 2021, 2022, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package webhooks 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "net/http" 11 "strings" 12 13 "github.com/gertd/go-pluralize" 14 "github.com/verrazzano/verrazzano/application-operator/constants" 15 "github.com/verrazzano/verrazzano/application-operator/controllers" 16 "github.com/verrazzano/verrazzano/application-operator/metricsexporter" 17 vzlog "github.com/verrazzano/verrazzano/pkg/log" 18 vzstring "github.com/verrazzano/verrazzano/pkg/string" 19 "go.uber.org/zap" 20 securityv1beta1 "istio.io/api/security/v1beta1" 21 "istio.io/api/type/v1beta1" 22 clisecurity "istio.io/client-go/pkg/apis/security/v1beta1" 23 istioversionedclient "istio.io/client-go/pkg/clientset/versioned" 24 corev1 "k8s.io/api/core/v1" 25 "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/client-go/dynamic" 29 "k8s.io/client-go/kubernetes" 30 "sigs.k8s.io/controller-runtime/pkg/client" 31 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 32 ) 33 34 // IstioDefaulterPath specifies the path of Istio defaulter webhook 35 const IstioDefaulterPath = "/istio-defaulter" 36 37 // IstioAppLabel label to be used for all pods that are istio enabled 38 const IstioAppLabel = "verrazzano.io/istio" 39 40 // IstioWebhook type for istio defaulter webhook 41 type IstioWebhook struct { 42 client.Client 43 IstioClient istioversionedclient.Interface 44 Decoder *admission.Decoder 45 KubeClient kubernetes.Interface 46 DynamicClient dynamic.Interface 47 } 48 49 // Handle is the entry point for the mutating webhook. 50 // This function is called for any pods that are created in a namespace with the label istio-injection=enabled. 51 func (a *IstioWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { 52 counterMetricObject, errorCounterMetricObject, handleDurationMetricObject, zapLogForMetrics, err := metricsexporter.ExposeControllerMetrics("IstioDefaulter", metricsexporter.IstioHandleCounter, metricsexporter.IstioHandleError, metricsexporter.IstioHandleDuration) 53 if err != nil { 54 return admission.Response{} 55 } 56 handleDurationMetricObject.TimerStart() 57 defer handleDurationMetricObject.TimerStop() 58 59 var log = zap.S().With(vzlog.FieldResourceNamespace, req.Namespace, vzlog.FieldResourceName, req.Name, vzlog.FieldWebhook, "istio-defaulter") 60 61 pod := &corev1.Pod{} 62 err = a.Decoder.Decode(req, pod) 63 if err != nil { 64 errorCounterMetricObject.Inc(zapLogForMetrics, err) 65 return admission.Errored(http.StatusBadRequest, err) 66 } 67 68 // Check for the annotation "sidecar.istio.io/inject: false". No action required if annotation is set to false. 69 for key, value := range pod.Annotations { 70 if key == "sidecar.istio.io/inject" && value == "false" { 71 log.Debugf("Pod labeled with sidecar.istio.io/inject: false: %s:%s:%s", req.Namespace, pod.Name, pod.GenerateName) 72 return admission.Allowed("No action required, pod labeled with sidecar.istio.io/inject: false") 73 } 74 } 75 76 // Get all owner references for this pod 77 ownerRefList, err := a.flattenOwnerReferences(nil, req.Namespace, pod.OwnerReferences, log) 78 if err != nil { 79 errorCounterMetricObject.Inc(zapLogForMetrics, err) 80 return admission.Errored(http.StatusInternalServerError, err) 81 } 82 83 // Check if the pod was created from an ApplicationConfiguration resource. 84 // We do this by checking for the existence of an ApplicationConfiguration ownerReference resource. 85 appConfigOwnerRef := metav1.OwnerReference{} 86 for _, ownerRef := range ownerRefList { 87 if ownerRef.Kind == "ApplicationConfiguration" { 88 appConfigOwnerRef = ownerRef 89 break 90 } 91 } 92 // No ApplicationConfiguration ownerReference resource was found so there is no action required. 93 if appConfigOwnerRef == (metav1.OwnerReference{}) { 94 log.Debugf("Pod is not a child of an ApplicationConfiguration: %s:%s:%s", req.Namespace, pod.Name, pod.GenerateName) 95 return admission.Allowed("No action required, pod is not a child of an ApplicationConfiguration resource") 96 } 97 98 // If a pod is using the "default" service account then create a app specific service account, if not already 99 // created. A service account is used as a principal in the Istio Authorization policy we create/update. 100 serviceAccountName := pod.Spec.ServiceAccountName 101 if serviceAccountName == "default" || serviceAccountName == "" { 102 serviceAccountName, err = a.createServiceAccount(req.Namespace, appConfigOwnerRef, log) 103 if err != nil { 104 errorCounterMetricObject.Inc(zapLogForMetrics, err) 105 return admission.Errored(http.StatusInternalServerError, err) 106 } 107 } 108 109 // Create/update Istio Authorization policy for the given pod. 110 err = a.createUpdateAuthorizationPolicy(req.Namespace, serviceAccountName, appConfigOwnerRef, pod.ObjectMeta.Labels, log) 111 if err != nil { 112 errorCounterMetricObject.Inc(zapLogForMetrics, err) 113 return admission.Errored(http.StatusInternalServerError, err) 114 } 115 116 // Fixup Istio Authorization policies within a project 117 ap := &AuthorizationPolicy{ 118 Client: a.Client, 119 IstioClient: a.IstioClient, 120 } 121 err = ap.fixupAuthorizationPoliciesForProjects(req.Namespace, log) 122 if err != nil { 123 errorCounterMetricObject.Inc(zapLogForMetrics, err) 124 return admission.Errored(http.StatusInternalServerError, err) 125 } 126 127 // Add the label to the pod which is used as the match selector in the authorization policy we created/updated. 128 if pod.Labels == nil { 129 pod.Labels = make(map[string]string) 130 } 131 pod.Labels[IstioAppLabel] = appConfigOwnerRef.Name 132 133 // Set the service account name for the pod which is used in the principal portion of the authorization policy we 134 // created/updated. 135 pod.Spec.ServiceAccountName = serviceAccountName 136 137 // Marshal the mutated pod to send back in the admission review response. 138 marshaledPod, err := json.Marshal(pod) 139 if err != nil { 140 errorCounterMetricObject.Inc(zapLogForMetrics, err) 141 return admission.Errored(http.StatusInternalServerError, err) 142 } 143 counterMetricObject.Inc(zapLogForMetrics, err) 144 return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) 145 } 146 147 // InjectDecoder injects the decoder. 148 func (a *IstioWebhook) InjectDecoder(d *admission.Decoder) error { 149 a.Decoder = d 150 return nil 151 } 152 153 // createUpdateAuthorizationPolicy will create/update an Istio authoriztion policy. 154 func (a *IstioWebhook) createUpdateAuthorizationPolicy(namespace string, serviceAccountName string, ownerRef metav1.OwnerReference, labels map[string]string, log *zap.SugaredLogger) error { 155 podPrincipal := fmt.Sprintf("cluster.local/ns/%s/sa/%s", namespace, serviceAccountName) 156 gwPrincipal := "cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account" 157 promPrincipal := "cluster.local/ns/verrazzano-system/sa/verrazzano-monitoring-operator" 158 weblogicOperPrincipal := "cluster.local/ns/verrazzano-system/sa/weblogic-operator-sa" 159 promOperatorPrincipal := "cluster.local/ns/verrazzano-monitoring/sa/prometheus-operator-kube-p-prometheus" 160 161 principals := []string{ 162 podPrincipal, 163 gwPrincipal, 164 promPrincipal, 165 promOperatorPrincipal, 166 } 167 // If the pod is WebLogic then add the WebLogic operator principle so that the operator can 168 // communicate with the WebLogic servers 169 workloadType, found := labels[constants.LabelWorkloadType] 170 weblogicFound := found && workloadType == constants.WorkloadTypeWeblogic 171 if weblogicFound { 172 principals = append(principals, weblogicOperPrincipal) 173 } 174 175 // Check if authorization policy exist. The name of the authorization policy is the owner reference name which happens 176 // to be the appconfig name. 177 authPolicy, err := a.IstioClient.SecurityV1beta1().AuthorizationPolicies(namespace).Get(context.TODO(), ownerRef.Name, metav1.GetOptions{}) 178 179 // If the authorization policy does not exist then we create it. 180 if err != nil && errors.IsNotFound(err) { 181 selector := v1beta1.WorkloadSelector{ 182 MatchLabels: map[string]string{ 183 IstioAppLabel: ownerRef.Name, 184 }, 185 } 186 fromRules := []*securityv1beta1.Rule_From{ 187 { 188 Source: &securityv1beta1.Source{ 189 Principals: principals, 190 }, 191 }, 192 } 193 194 ap := &clisecurity.AuthorizationPolicy{ 195 ObjectMeta: metav1.ObjectMeta{ 196 Name: ownerRef.Name, 197 Namespace: namespace, 198 Labels: map[string]string{ 199 IstioAppLabel: ownerRef.Name, 200 }, 201 OwnerReferences: []metav1.OwnerReference{ 202 { 203 Name: ownerRef.Name, 204 Kind: ownerRef.Kind, 205 APIVersion: ownerRef.APIVersion, 206 UID: ownerRef.UID, 207 }, 208 }, 209 }, 210 Spec: securityv1beta1.AuthorizationPolicy{ 211 Selector: &selector, 212 Rules: []*securityv1beta1.Rule{ 213 { 214 From: fromRules, 215 }, 216 }, 217 }, 218 } 219 220 log.Infof("Creating Istio authorization policy: %s:%s", namespace, ownerRef.Name) 221 _, err := a.IstioClient.SecurityV1beta1().AuthorizationPolicies(namespace).Create(context.TODO(), ap, metav1.CreateOptions{}) 222 return err 223 } else if err != nil { 224 return err 225 } 226 227 // If the pod and/or WebLogic operator principals are missing then update the principal list 228 principalSet := vzstring.SliceToSet(authPolicy.Spec.GetRules()[0].From[0].Source.Principals) 229 var update bool 230 if _, ok := principalSet[podPrincipal]; !ok { 231 update = true 232 authPolicy.Spec.GetRules()[0].From[0].Source.Principals = append(authPolicy.Spec.GetRules()[0].From[0].Source.Principals, podPrincipal) 233 } 234 if weblogicFound { 235 if _, ok := principalSet[weblogicOperPrincipal]; !ok { 236 update = true 237 authPolicy.Spec.GetRules()[0].From[0].Source.Principals = append(authPolicy.Spec.GetRules()[0].From[0].Source.Principals, weblogicOperPrincipal) 238 } 239 } 240 // Update the policy with the principals that are missing 241 if update { 242 log.Debugf("Updating Istio authorization policy: %s:%s", namespace, ownerRef.Name) 243 _, err := a.IstioClient.SecurityV1beta1().AuthorizationPolicies(namespace).Update(context.TODO(), authPolicy, metav1.UpdateOptions{}) 244 if err != nil { 245 return err 246 } 247 } 248 return nil 249 } 250 251 // createServiceAccount will create a service account to be referenced by the Istio authorization policy 252 func (a *IstioWebhook) createServiceAccount(namespace string, ownerRef metav1.OwnerReference, log *zap.SugaredLogger) (string, error) { 253 // Check if service account exist. The name of the service account is the owner reference name which happens 254 // to be the appconfig name. 255 serviceAccount, err := a.KubeClient.CoreV1().ServiceAccounts(namespace).Get(context.TODO(), ownerRef.Name, metav1.GetOptions{}) 256 257 // If the service account does not exist then we create it. 258 if err != nil && errors.IsNotFound(err) { 259 sa := &corev1.ServiceAccount{ 260 ObjectMeta: metav1.ObjectMeta{ 261 Name: ownerRef.Name, 262 Namespace: namespace, 263 Labels: map[string]string{ 264 IstioAppLabel: ownerRef.Name, 265 }, 266 OwnerReferences: []metav1.OwnerReference{ 267 { 268 Name: ownerRef.Name, 269 Kind: ownerRef.Kind, 270 APIVersion: ownerRef.APIVersion, 271 UID: ownerRef.UID, 272 }, 273 }, 274 }, 275 } 276 log.Debugf("Creating service account: %s:%s", namespace, ownerRef.Name) 277 serviceAccount, err = a.KubeClient.CoreV1().ServiceAccounts(namespace).Create(context.TODO(), sa, metav1.CreateOptions{}) 278 if err != nil { 279 return "", err 280 } 281 } else if err != nil { 282 return "", err 283 } 284 285 return serviceAccount.Name, nil 286 } 287 288 // flattenOwnerReferences traverses a nested array of owner references and returns a single array of owner references. 289 func (a *IstioWebhook) flattenOwnerReferences(list []metav1.OwnerReference, namespace string, ownerRefs []metav1.OwnerReference, log *zap.SugaredLogger) ([]metav1.OwnerReference, error) { 290 for _, ownerRef := range ownerRefs { 291 list = append(list, ownerRef) 292 293 group, version := controllers.ConvertAPIVersionToGroupAndVersion(ownerRef.APIVersion) 294 resource := schema.GroupVersionResource{ 295 Group: group, 296 Version: version, 297 Resource: pluralize.NewClient().Plural(strings.ToLower(ownerRef.Kind)), 298 } 299 300 unst, err := a.DynamicClient.Resource(resource).Namespace(namespace).Get(context.TODO(), ownerRef.Name, metav1.GetOptions{}) 301 if err != nil { 302 if !errors.IsNotFound(err) { 303 log.Errorf("Failed getting the Dynamic API: %v", err) 304 } 305 return nil, err 306 } 307 308 if len(unst.GetOwnerReferences()) != 0 { 309 list, err = a.flattenOwnerReferences(list, namespace, unst.GetOwnerReferences(), log) 310 if err != nil { 311 return nil, err 312 } 313 } 314 } 315 return list, nil 316 }