...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/cert/certclient/certclient.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/cert/certclient

     1  // Copyright 2022 Google LLC
     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 certclient
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"time"
    22  
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/cert/provisioner"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/webhook/cert/writer"
    25  
    26  	admissionregistration "k8s.io/api/admissionregistration/v1"
    27  	corev1 "k8s.io/api/core/v1"
    28  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    29  	"k8s.io/apimachinery/pkg/util/wait"
    30  	"k8s.io/klog/v2"
    31  	"sigs.k8s.io/controller-runtime/pkg/client"
    32  )
    33  
    34  const (
    35  	maxJitterFactor = 0.1
    36  )
    37  
    38  // default interval for checking cert is 90 days (~3 months)
    39  var defaultCertRefreshInterval = 3 * 30 * 24 * time.Hour
    40  
    41  // CertClient is responsible for installing webhook manifests and provisioning and rotating their certs.
    42  type CertClient struct {
    43  	webhookManifests []client.Object
    44  	svc              *corev1.Service
    45  	kubeClient       client.Client
    46  	provisioner      *provisioner.Provisioner
    47  }
    48  
    49  type Options struct {
    50  	WebhookManifests []client.Object
    51  	Service          *corev1.Service
    52  	KubeClient       client.Client
    53  	CertDir          string
    54  	CertWriter       writer.CertWriter
    55  }
    56  
    57  func New(opts Options) (*CertClient, error) {
    58  	certWriter, err := certWriterFromOptsOrNew(opts)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	return &CertClient{
    63  		webhookManifests: opts.WebhookManifests,
    64  		svc:              opts.Service,
    65  		provisioner: &provisioner.Provisioner{
    66  			CertWriter: certWriter,
    67  		},
    68  		kubeClient: opts.KubeClient,
    69  	}, nil
    70  }
    71  
    72  func certWriterFromOptsOrNew(opts Options) (writer.CertWriter, error) {
    73  	if opts.CertWriter != nil {
    74  		return opts.CertWriter, nil
    75  	}
    76  	certWriter, err := writer.NewFSCertWriter(
    77  		writer.FSCertWriterOptions{
    78  			Path: opts.CertDir,
    79  		})
    80  	if err != nil {
    81  		return nil, fmt.Errorf("error creating FS cert writer: %w", err)
    82  	}
    83  	return certWriter, nil
    84  }
    85  
    86  func (c *CertClient) RefreshCertsAndInstall() error {
    87  	_, err := c.provisioner.Provision(provisioner.Options{
    88  		ClientConfig: &admissionregistration.WebhookClientConfig{
    89  			CABundle: []byte{},
    90  			Service: &admissionregistration.ServiceReference{
    91  				Name:      c.svc.GetName(),
    92  				Namespace: c.svc.GetNamespace(),
    93  			},
    94  		},
    95  		Objects: c.webhookManifests,
    96  	})
    97  	if err != nil {
    98  		return fmt.Errorf("error provisioning certs: %w", err)
    99  	}
   100  	objects := append([]client.Object{c.svc}, c.webhookManifests...)
   101  	return batchCreateOrReplace(c.kubeClient, objects...)
   102  }
   103  
   104  func (c *CertClient) Start(ctx context.Context) error {
   105  	timer := time.Tick(wait.Jitter(defaultCertRefreshInterval, maxJitterFactor))
   106  	for {
   107  		select {
   108  		case <-timer:
   109  			if err := c.RefreshCertsAndInstall(); err != nil {
   110  				return fmt.Errorf("error refreshing certs: %w", err)
   111  			}
   112  
   113  			// We force-exit to reload the certs.
   114  			// We are missing logic to apply the new certs here;
   115  			// it's also possible that another pod rotated this cert.
   116  			// Because we want to move this to the operator anyway,
   117  			// we simply exit here (relying on kubelet to restart us)
   118  			// rather than trying to add update logic.
   119  			// b/267353534
   120  			klog.Warningf("forcing process exit after ~%v to reload webhook certificates", defaultCertRefreshInterval)
   121  			os.Exit(1)
   122  
   123  		case <-ctx.Done():
   124  			return nil
   125  		}
   126  	}
   127  }
   128  
   129  type mutateFn func(current, desired *client.Object) error
   130  
   131  var serviceFn = func(current, desired *client.Object) error {
   132  	typedC := (*current).(*corev1.Service)
   133  	typedD := (*desired).(*corev1.Service)
   134  	typedC.Spec.Selector = typedD.Spec.Selector
   135  	typedC.Spec.Ports = typedD.Spec.Ports
   136  	return nil
   137  }
   138  
   139  var mutatingWebhookConfigFn = func(current, desired *client.Object) error {
   140  	typedC := (*current).(*admissionregistration.MutatingWebhookConfiguration)
   141  	typedD := (*desired).(*admissionregistration.MutatingWebhookConfiguration)
   142  	typedC.Webhooks = typedD.Webhooks
   143  	return nil
   144  }
   145  
   146  var validatingWebhookConfigFn = func(current, desired *client.Object) error {
   147  	typedC := (*current).(*admissionregistration.ValidatingWebhookConfiguration)
   148  	typedD := (*desired).(*admissionregistration.ValidatingWebhookConfiguration)
   149  	typedC.Webhooks = typedD.Webhooks
   150  	return nil
   151  }
   152  
   153  var genericFn = func(current, desired *client.Object) error {
   154  	*current = *desired
   155  	return nil
   156  }
   157  
   158  // createOrReplaceHelper creates the object if it doesn't exist;
   159  // otherwise, it will replace it.
   160  // When replacing, fn  should know how to preserve existing fields in the object GET from the APIServer.
   161  func createOrReplaceHelper(c client.Client, obj client.Object, fn mutateFn) error {
   162  	if obj == nil {
   163  		return nil
   164  	}
   165  	err := c.Create(context.Background(), obj)
   166  	if apierrors.IsAlreadyExists(err) {
   167  		// TODO: retry mutiple times with backoff if necessary.
   168  		// this cast is not safe per-say but is added to get around the transition from runtime.Object to client.Object
   169  		existing, ok := obj.DeepCopyObject().(client.Object)
   170  		if !ok {
   171  			return fmt.Errorf("unable to cast deep copy to client.Object")
   172  		}
   173  		objectKey := client.ObjectKeyFromObject(obj)
   174  		err = c.Get(context.Background(), objectKey, existing)
   175  		if err != nil {
   176  			return err
   177  		}
   178  		err = fn(&existing, &obj)
   179  		if err != nil {
   180  			return err
   181  		}
   182  		return c.Update(context.Background(), existing)
   183  	}
   184  	return err
   185  }
   186  
   187  // createOrReplace creates the object if it doesn't exist;
   188  // otherwise, it will replace it.
   189  // When replacing, it knows how to preserve existing fields in the object GET from the APIServer.
   190  // It currently only support MutatingWebhookConfiguration, ValidatingWebhookConfiguration and Service.
   191  // For other kinds, it uses genericFn to replace the whole object.
   192  func createOrReplace(c client.Client, obj client.Object) error {
   193  	if obj == nil {
   194  		return nil
   195  	}
   196  	switch obj.(type) {
   197  	case *admissionregistration.MutatingWebhookConfiguration:
   198  		return createOrReplaceHelper(c, obj, mutatingWebhookConfigFn)
   199  	case *admissionregistration.ValidatingWebhookConfiguration:
   200  		return createOrReplaceHelper(c, obj, validatingWebhookConfigFn)
   201  	case *corev1.Service:
   202  		return createOrReplaceHelper(c, obj, serviceFn)
   203  	default:
   204  		return createOrReplaceHelper(c, obj, genericFn)
   205  	}
   206  }
   207  
   208  func batchCreateOrReplace(c client.Client, objs ...client.Object) error {
   209  	for i := range objs {
   210  		err := createOrReplace(c, objs[i])
   211  		if err != nil {
   212  			return err
   213  		}
   214  	}
   215  	return nil
   216  }
   217  

View as plain text