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  }