images/kube-webhook-certgen/rootfs: add support for patching APIService objects (#7641)
* images/kube-webhook-certgen/rootfs/pkg/k8s: return err from functions Initially only from some to preserve existing behavior. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs: make patching return error So we don't call log.Fatal in so many places, which makes code testable. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/pkg/k8s: require context So initialize top-level contexts in tests and CLI, then pass them around all the way down, so there is an ability e.g. to add timeouts to patch operations, if needed and to follow general conventions. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/pkg/k8s: support patching APIService APIService object is very similar to MutatingWebhookConfiguration and ValidatingWebhookConfiguration objects, so support for patching it shouldn't be too much of a burden. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: use new patch API So old function PatchWebhookConfigurations can be unexported and CLI can be extended to also support patching APIService. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/pkg/k8s: unexport old patch function PatchObjects should be now used instead. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs: add .gitignore To ignore manually built binaries during development process. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: test patching By adding a PatchConfig and Patch function, it is now possible to test logic of flag validation, which was previously tied to CLI options. This commit adds nice set of tests covering existing logic. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: improve formatting Those strings will be changed anyway in future commits, so at first we can properly capitalize used names. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs/cmd: support patching APIService As logic for creating a CA certificate and patching an object is almost the same for both webhook configuration and API services, this commit adds support to kube-webhook-certgen CLI to also patch APIService objects, so they can be served over TLS as well. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com> * images/kube-webhook-certgen/rootfs: pass failure policy by value k8s.k8s.patchWebhookConfigurations() always dereferences it and we do not do a nil check, so the code may panic in some conditions, so it's safer to just pass it by value, as it's just a wrapped string. Signed-off-by: Mateusz Gozdek <mgozdek@microsoft.com>
This commit is contained in:
parent
5662db4509
commit
9acf62d867
9 changed files with 723 additions and 60 deletions
|
|
@ -2,6 +2,7 @@ package k8s
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
admissionv1 "k8s.io/api/admissionregistration/v1"
|
||||
|
|
@ -9,103 +10,214 @@ import (
|
|||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
|
||||
)
|
||||
|
||||
type k8s struct {
|
||||
clientset kubernetes.Interface
|
||||
clientset kubernetes.Interface
|
||||
aggregatorClientset clientset.Interface
|
||||
}
|
||||
|
||||
func New(clientset kubernetes.Interface) *k8s {
|
||||
func New(clientset kubernetes.Interface, aggregatorClientset clientset.Interface) *k8s {
|
||||
if clientset == nil {
|
||||
log.Fatal("no kubernetes client given")
|
||||
}
|
||||
|
||||
return &k8s{
|
||||
clientset: clientset,
|
||||
if aggregatorClientset == nil {
|
||||
log.Fatal("no kubernetes aggregator client given")
|
||||
}
|
||||
|
||||
return &k8s{
|
||||
clientset: clientset,
|
||||
aggregatorClientset: aggregatorClientset,
|
||||
}
|
||||
}
|
||||
|
||||
type PatchOptions struct {
|
||||
ValidatingWebhookConfigurationName string
|
||||
MutatingWebhookConfigurationName string
|
||||
APIServiceName string
|
||||
CABundle []byte
|
||||
FailurePolicyType admissionv1.FailurePolicyType
|
||||
}
|
||||
|
||||
func (k8s *k8s) PatchObjects(ctx context.Context, options PatchOptions) error {
|
||||
patchMutating := options.MutatingWebhookConfigurationName != ""
|
||||
patchValidating := options.ValidatingWebhookConfigurationName != ""
|
||||
patchAPIService := options.APIServiceName != ""
|
||||
|
||||
if !patchMutating && !patchValidating && options.FailurePolicyType != "" {
|
||||
return fmt.Errorf("failurePolicy specified, but no webhook will be patched")
|
||||
}
|
||||
|
||||
if options.MutatingWebhookConfigurationName != options.ValidatingWebhookConfigurationName {
|
||||
return fmt.Errorf("webhook names must be the same")
|
||||
}
|
||||
|
||||
if patchAPIService {
|
||||
log.Infof("patching APIService %q", options.APIServiceName)
|
||||
|
||||
if err := k8s.patchAPIService(ctx, options.APIServiceName, options.CABundle); err != nil {
|
||||
// Intentionally don't wrap error here to preserve old behavior and be able to log both
|
||||
// original error and a message.
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if patchMutating || patchValidating {
|
||||
return k8s.patchWebhookConfigurations(ctx, options.ValidatingWebhookConfigurationName, options.CABundle, options.FailurePolicyType, patchMutating, patchValidating)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k8s *k8s) patchAPIService(ctx context.Context, objectName string, ca []byte) error {
|
||||
log.Infof("patching APIService %q", objectName)
|
||||
|
||||
c := k8s.aggregatorClientset.ApiregistrationV1().APIServices()
|
||||
|
||||
apiService, err := c.Get(ctx, objectName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return &wrappedError{
|
||||
err: err,
|
||||
message: fmt.Sprintf("failed getting APIService %q", objectName),
|
||||
}
|
||||
}
|
||||
|
||||
apiService.Spec.CABundle = ca
|
||||
apiService.Spec.InsecureSkipTLSVerify = false
|
||||
|
||||
if _, err := c.Update(ctx, apiService, metav1.UpdateOptions{}); err != nil {
|
||||
return &wrappedError{
|
||||
err: err,
|
||||
message: fmt.Sprintf("failed patching APIService %q", objectName),
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug("patched APIService")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PatchWebhookConfigurations will patch validatingWebhook and mutatingWebhook clientConfig configurations with
|
||||
// the provided ca data. If failurePolicy is provided, patch all webhooks with this value
|
||||
func (k8s *k8s) PatchWebhookConfigurations(
|
||||
func (k8s *k8s) patchWebhookConfigurations(
|
||||
ctx context.Context,
|
||||
configurationName string,
|
||||
ca []byte,
|
||||
failurePolicy *admissionv1.FailurePolicyType,
|
||||
failurePolicy admissionv1.FailurePolicyType,
|
||||
patchMutating bool,
|
||||
patchValidating bool,
|
||||
) {
|
||||
log.Infof("patching webhook configurations '%s' mutating=%t, validating=%t, failurePolicy=%s", configurationName, patchMutating, patchValidating, *failurePolicy)
|
||||
) error {
|
||||
log.Infof("patching webhook configurations '%s' mutating=%t, validating=%t, failurePolicy=%s", configurationName, patchMutating, patchValidating, failurePolicy)
|
||||
|
||||
if patchValidating {
|
||||
k8s.patchValidating(configurationName, ca, failurePolicy)
|
||||
if err := k8s.patchValidating(ctx, configurationName, ca, failurePolicy); err != nil {
|
||||
// Intentionally don't wrap error here to preserve old behavior and be able to log both original error and a message.
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("validating hook patching not required")
|
||||
}
|
||||
|
||||
if patchMutating {
|
||||
k8s.patchMutating(configurationName, ca, failurePolicy)
|
||||
if err := k8s.patchMutating(ctx, configurationName, ca, failurePolicy); err != nil {
|
||||
// Intentionally don't wrap error here to preserve old behavior and be able to log both original error and a message.
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Debug("mutating hook patching not required")
|
||||
}
|
||||
|
||||
log.Info("Patched hook(s)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k8s *k8s) patchValidating(configurationName string, ca []byte, failurePolicy *admissionv1.FailurePolicyType) {
|
||||
type wrappedError struct {
|
||||
err error
|
||||
message string
|
||||
}
|
||||
|
||||
func (err wrappedError) Error() string {
|
||||
return err.message
|
||||
}
|
||||
|
||||
func (err wrappedError) Unwrap() error {
|
||||
return err.err
|
||||
}
|
||||
|
||||
func (k8s *k8s) patchValidating(ctx context.Context, configurationName string, ca []byte, failurePolicy admissionv1.FailurePolicyType) error {
|
||||
valHook, err := k8s.clientset.
|
||||
AdmissionregistrationV1().
|
||||
ValidatingWebhookConfigurations().
|
||||
Get(context.TODO(), configurationName, metav1.GetOptions{})
|
||||
Get(ctx, configurationName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.WithField("err", err).Fatal("failed getting validating webhook")
|
||||
return &wrappedError{
|
||||
err: err,
|
||||
message: "failed getting validating webhook",
|
||||
}
|
||||
}
|
||||
|
||||
for i := range valHook.Webhooks {
|
||||
h := &valHook.Webhooks[i]
|
||||
h.ClientConfig.CABundle = ca
|
||||
if *failurePolicy != "" {
|
||||
h.FailurePolicy = failurePolicy
|
||||
if failurePolicy != "" {
|
||||
h.FailurePolicy = &failurePolicy
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = k8s.clientset.AdmissionregistrationV1().
|
||||
ValidatingWebhookConfigurations().
|
||||
Update(context.TODO(), valHook, metav1.UpdateOptions{}); err != nil {
|
||||
log.WithField("err", err).Fatal("failed patching validating webhook")
|
||||
Update(ctx, valHook, metav1.UpdateOptions{}); err != nil {
|
||||
return &wrappedError{
|
||||
err: err,
|
||||
message: "failed patching validating webhook",
|
||||
}
|
||||
}
|
||||
log.Debug("patched validating hook")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k8s *k8s) patchMutating(configurationName string, ca []byte, failurePolicy *admissionv1.FailurePolicyType) {
|
||||
func (k8s *k8s) patchMutating(ctx context.Context, configurationName string, ca []byte, failurePolicy admissionv1.FailurePolicyType) error {
|
||||
mutHook, err := k8s.clientset.
|
||||
AdmissionregistrationV1().
|
||||
MutatingWebhookConfigurations().
|
||||
Get(context.TODO(), configurationName, metav1.GetOptions{})
|
||||
Get(ctx, configurationName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.WithField("err", err).Fatal("failed getting mutating webhook")
|
||||
return &wrappedError{
|
||||
err: err,
|
||||
message: "failed getting mutating webhook",
|
||||
}
|
||||
}
|
||||
|
||||
for i := range mutHook.Webhooks {
|
||||
h := &mutHook.Webhooks[i]
|
||||
h.ClientConfig.CABundle = ca
|
||||
if *failurePolicy != "" {
|
||||
h.FailurePolicy = failurePolicy
|
||||
if failurePolicy != "" {
|
||||
h.FailurePolicy = &failurePolicy
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = k8s.clientset.AdmissionregistrationV1().
|
||||
MutatingWebhookConfigurations().
|
||||
Update(context.TODO(), mutHook, metav1.UpdateOptions{}); err != nil {
|
||||
log.WithField("err", err).Fatal("failed patching mutating webhook")
|
||||
Update(ctx, mutHook, metav1.UpdateOptions{}); err != nil {
|
||||
return &wrappedError{
|
||||
err: err,
|
||||
message: "failed patching mutating webhook",
|
||||
}
|
||||
}
|
||||
log.Debug("patched mutating hook")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCaFromSecret will check for the presence of a secret. If it exists, will return the content of the
|
||||
// "ca" from the secret, otherwise will return nil
|
||||
func (k8s *k8s) GetCaFromSecret(secretName string, namespace string) []byte {
|
||||
func (k8s *k8s) GetCaFromSecret(ctx context.Context, secretName string, namespace string) []byte {
|
||||
log.Debugf("getting secret '%s' in namespace '%s'", secretName, namespace)
|
||||
secret, err := k8s.clientset.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, metav1.GetOptions{})
|
||||
secret, err := k8s.clientset.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if k8serrors.IsNotFound(err) {
|
||||
log.WithField("err", err).Info("no secret found")
|
||||
|
|
@ -123,7 +235,7 @@ func (k8s *k8s) GetCaFromSecret(secretName string, namespace string) []byte {
|
|||
}
|
||||
|
||||
// SaveCertsToSecret saves the provided ca, cert and key into a secret in the specified namespace.
|
||||
func (k8s *k8s) SaveCertsToSecret(secretName, namespace, certName, keyName string, ca, cert, key []byte) {
|
||||
func (k8s *k8s) SaveCertsToSecret(ctx context.Context, secretName, namespace, certName, keyName string, ca, cert, key []byte) {
|
||||
log.Debugf("saving to secret '%s' in namespace '%s'", secretName, namespace)
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
|
@ -133,7 +245,7 @@ func (k8s *k8s) SaveCertsToSecret(secretName, namespace, certName, keyName strin
|
|||
}
|
||||
|
||||
log.Debug("saving secret")
|
||||
_, err := k8s.clientset.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
|
||||
_, err := k8s.clientset.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
log.WithField("err", err).Fatal("failed creating secret")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue