Skip to content

Commit

Permalink
webhookcontroller: report when a webhook resource is missing a caBund…
Browse files Browse the repository at this point in the history
…le provided by the service-ca-operator
  • Loading branch information
p0lyn0mial committed Dec 18, 2023
1 parent 119a16f commit aa5d5ed
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 18 deletions.
23 changes: 21 additions & 2 deletions pkg/operator/webhooksupportabilitycontroller/degraded_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (
"k8s.io/klog/v2"
)

const (
// injectCABundleAnnotationName is the annotation used by the
// service-ca-operator to indicate which resources it should inject the CA into.
injectCABundleAnnotationName = "service.beta.openshift.io/inject-cabundle"
)

// webhookInfo generically represents a webhook
type webhookInfo struct {
Name string
Expand All @@ -25,6 +31,9 @@ type webhookInfo struct {
// TimeoutSeconds specifies the timeout for a webhook.
// After the timeout passes, the webhook call will be ignored or the API call will fail
TimeoutSeconds *int32
// HasServiceCaAnnotation indicates whether a webhook
// resource has been annotated for CABundle injection by the service-ca-operator
HasServiceCaAnnotation bool
}

// serviceReference generically represents a service reference
Expand Down Expand Up @@ -52,7 +61,7 @@ func (c *webhookSupportabilityController) updateWebhookConfigurationDegraded(ctx
serviceMsgs = append(serviceMsgs, msg)
continue
}
err = c.assertConnect(ctx, webhook.Name, webhook.Service, webhook.CABundle, webhook.TimeoutSeconds)
err = c.assertConnect(ctx, webhook.Name, webhook.Service, webhook.CABundle, webhook.HasServiceCaAnnotation, webhook.TimeoutSeconds)
if err != nil {
msg := fmt.Sprintf("%s: %s", webhook.Name, err)
if webhook.FailurePolicyIsIgnore {
Expand Down Expand Up @@ -97,7 +106,7 @@ func (c *webhookSupportabilityController) assertService(reference *serviceRefere
}

// assertConnect performs a dns lookup of service, opens a tcp connection, and performs a tls handshake.
func (c *webhookSupportabilityController) assertConnect(ctx context.Context, webhookName string, reference *serviceReference, caBundle []byte, webhookTimeoutSeconds *int32) error {
func (c *webhookSupportabilityController) assertConnect(ctx context.Context, webhookName string, reference *serviceReference, caBundle []byte, caBundleProvidedByServiceCA bool, webhookTimeoutSeconds *int32) error {
host := reference.Name + "." + reference.Namespace + ".svc"
port := "443"
if reference.Port != nil {
Expand All @@ -106,6 +115,9 @@ func (c *webhookSupportabilityController) assertConnect(ctx context.Context, web
rootCAs := x509.NewCertPool()
if len(caBundle) > 0 {
rootCAs.AppendCertsFromPEM(caBundle)
} else if caBundleProvidedByServiceCA {
err := fmt.Errorf("skipping checking the webhook via %q service because the caBundle (provided by the service-ca-operator) is empty. Please check the service-ca's logs if the issue persists", net.JoinHostPort(host, port))
return err
}
timeout := 10 * time.Second
if webhookTimeoutSeconds != nil {
Expand Down Expand Up @@ -142,3 +154,10 @@ func (c *webhookSupportabilityController) assertConnect(ctx context.Context, web
}
return err
}

func hasServiceCaAnnotation(annotations map[string]string) bool {
if annotations == nil {
return false
}
return strings.EqualFold(annotations[injectCABundleAnnotationName], "true")
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ func (c *webhookSupportabilityController) updateMutatingAdmissionWebhookConfigur
for _, webhookConfiguration := range webhookConfigurations {
for _, webhook := range webhookConfiguration.Webhooks {
info := webhookInfo{
Name: webhook.Name,
CABundle: webhook.ClientConfig.CABundle,
FailurePolicyIsIgnore: webhook.FailurePolicy != nil && *webhook.FailurePolicy == admissionregistrationv1.Ignore,
TimeoutSeconds: webhook.TimeoutSeconds,
Name: webhook.Name,
CABundle: webhook.ClientConfig.CABundle,
HasServiceCaAnnotation: hasServiceCaAnnotation(webhookConfiguration.Annotations),
FailurePolicyIsIgnore: webhook.FailurePolicy != nil && *webhook.FailurePolicy == admissionregistrationv1.Ignore,
TimeoutSeconds: webhook.TimeoutSeconds,
}
if webhook.ClientConfig.Service != nil {
info.Service = &serviceReference{
Expand Down Expand Up @@ -56,10 +57,11 @@ func (c *webhookSupportabilityController) updateValidatingAdmissionWebhookConfig
for _, webhookConfiguration := range webhookConfigurations {
for _, webhook := range webhookConfiguration.Webhooks {
info := webhookInfo{
Name: webhook.Name,
CABundle: webhook.ClientConfig.CABundle,
FailurePolicyIsIgnore: webhook.FailurePolicy != nil && (*webhook.FailurePolicy == v1.Ignore),
TimeoutSeconds: webhook.TimeoutSeconds,
Name: webhook.Name,
CABundle: webhook.ClientConfig.CABundle,
HasServiceCaAnnotation: hasServiceCaAnnotation(webhookConfiguration.Annotations),
FailurePolicyIsIgnore: webhook.FailurePolicy != nil && (*webhook.FailurePolicy == v1.Ignore),
TimeoutSeconds: webhook.TimeoutSeconds,
}

if webhook.ClientConfig.Service != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,27 @@ func TestUpdateMutatingAdmissionWebhookConfigurationDegraded(t *testing.T) {
Message: `mw10: (?:.*)?x509: certificate signed by unknown authority`,
},
},
{
name: "CABundleNotYetProvided",
webhookConfigs: []*admissionregistrationv1.MutatingWebhookConfiguration{
mutatingWebhookConfiguration("mwc10",
withMutatingWebhook("mw10", withMutatingServiceReference("ns10", "svc10")),
withMutatingWebhookAnnotatedWithServiceCABundleInjection,
),
},
services: []*corev1.Service{
service("ns10", "svc10"),
},
webhookServers: []*mockWebhookServer{
webhookServer("mwc10", "ns10", "svc10", withEmptyCABundle),
},
expected: operatorv1.OperatorCondition{
Type: MutatingAdmissionWebhookConfigurationErrorType,
Status: operatorv1.ConditionTrue,
Reason: WebhookServiceConnectionErrorReason,
Message: `mw10: skipping checking the webhook via \"([^"]+)\" service because the caBundle \(provided by the service-ca-operator\) is empty. Please check the service-ca's logs if the issue persists`,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -385,6 +406,27 @@ func TestUpdateValidatingAdmissionWebhookConfigurationDegradedStatus(t *testing.
Message: `mw10: (?:.*)?x509: certificate signed by unknown authority`,
},
},
{
name: "CABundleNotYetProvided",
webhookConfigs: []*admissionregistrationv1.ValidatingWebhookConfiguration{
validatingWebhookConfiguration("mwc10",
withValidatingWebhook("mw10", withValidatingServiceReference("ns10", "svc10")),
withValidatingWebhookAnnotatedWithServiceCABundleInjection,
),
},
services: []*corev1.Service{
service("ns10", "svc10"),
},
webhookServers: []*mockWebhookServer{
webhookServer("mwc10", "ns10", "svc10", withEmptyCABundle),
},
expected: operatorv1.OperatorCondition{
Type: ValidatingAdmissionWebhookConfigurationErrorType,
Status: operatorv1.ConditionTrue,
Reason: WebhookServiceConnectionErrorReason,
Message: `mw10: skipping checking the webhook via \"([^"]+)\" service because the caBundle \(provided by the service-ca-operator\) is empty. Please check the service-ca's logs if the issue persists`,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -466,6 +508,13 @@ func mutatingWebhookConfiguration(n string, options ...func(*admissionregistrati
return c
}

func withMutatingWebhookAnnotatedWithServiceCABundleInjection(c *admissionregistrationv1.MutatingWebhookConfiguration) {
if c.Annotations == nil {
c.Annotations = map[string]string{}
}
c.Annotations[injectCABundleAnnotationName] = "true"
}

func withMutatingWebhook(n string, options ...func(*admissionregistrationv1.MutatingWebhook)) func(*admissionregistrationv1.MutatingWebhookConfiguration) {
return func(c *admissionregistrationv1.MutatingWebhookConfiguration) {
w := &admissionregistrationv1.MutatingWebhook{
Expand Down Expand Up @@ -554,6 +603,13 @@ func withValidatingServiceReference(ns, n string) func(*admissionregistrationv1.
}
}

func withValidatingWebhookAnnotatedWithServiceCABundleInjection(c *admissionregistrationv1.ValidatingWebhookConfiguration) {
if c.Annotations == nil {
c.Annotations = map[string]string{}
}
c.Annotations[injectCABundleAnnotationName] = "true"
}

func service(ns, n string) *corev1.Service {
s := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: ns, Name: n},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ func (c *webhookSupportabilityController) updateCRDConversionWebhookConfiguratio
continue
}
info := webhookInfo{
Name: crd.Name,
CABundle: crd.Spec.Conversion.Webhook.ClientConfig.CABundle,
Name: crd.Name,
CABundle: crd.Spec.Conversion.Webhook.ClientConfig.CABundle,
HasServiceCaAnnotation: hasServiceCaAnnotation(crd.Annotations),
Service: &serviceReference{
Namespace: crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace,
Name: crd.Spec.Conversion.Webhook.ClientConfig.Service.Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,27 @@ func TestUpdateCRDConversionWebhookConfigurationDegraded(t *testing.T) {
Message: `crd10: (?:.*)?x509: certificate signed by unknown authority`,
},
},
{
name: "CABundleNotYetProvided",
crds: []*apiextensionsv1.CustomResourceDefinition{
customResourceDefinition("crd10",
withConversionServiceReference("ns10", "svc10"),
withCRDAnnotatedWithServiceCABundleInjection,
),
},
services: []*corev1.Service{
service("ns10", "svc10"),
},
webhookServers: []*mockWebhookServer{
webhookServer("crd10", "ns10", "svc10", withEmptyCABundle),
},
expected: operatorv1.OperatorCondition{
Type: CRDConversionWebhookConfigurationErrorType,
Status: operatorv1.ConditionTrue,
Reason: WebhookServiceConnectionErrorReason,
Message: `crd10: skipping checking the webhook via \"([^"]+)\" service because the caBundle \(provided by the service-ca-operator\) is empty. Please check the service-ca's logs if the issue persists`,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -242,3 +263,10 @@ func withConversionServiceReference(ns, n string) func(*apiextensionsv1.CustomRe
crd.Spec.Conversion = c
}
}

func withCRDAnnotatedWithServiceCABundleInjection(crd *apiextensionsv1.CustomResourceDefinition) {
if crd.Annotations == nil {
crd.Annotations = map[string]string{}
}
crd.Annotations[injectCABundleAnnotationName] = "true"
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,18 @@ func withWrongCABundle(t *testing.T) func(*mockWebhookServer) {
}
}

func withEmptyCABundle(s *mockWebhookServer) {
s.skipCABundleInjection = true
}

type mockWebhookServer struct {
Config string
Service serviceReference
Hostname string
Port *int32
CABundle []byte
doNotStart bool
Config string
Service serviceReference
Hostname string
Port *int32
CABundle []byte
skipCABundleInjection bool
doNotStart bool
}

// Run starts the mock server. Port and CABundle are available after this method returns.
Expand All @@ -128,6 +133,12 @@ func (s *mockWebhookServer) Run(t *testing.T, ctx context.Context) {
rootCA := &crypto.CA{SerialGenerator: &crypto.RandomSerialGenerator{}, Config: rootCACertCfg}
if len(s.CABundle) == 0 {
s.CABundle, _, err = rootCA.Config.GetPEMBytes()
if err != nil {
t.Fatal(err)
}
}
if s.skipCABundleInjection {
s.CABundle = []byte{}
}
// server certs
serverCertCfg, err := rootCA.MakeServerCert(sets.NewString(s.Hostname, "127.0.0.1"), 10)
Expand Down

0 comments on commit aa5d5ed

Please sign in to comment.