Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ helm-unittest:
test-e2e: controller-image audit-scanner-image policy-server-image
$(GO_BUILD_ENV) go test ./e2e/ -v

.PHONY: test-all
test-all: test helm-unittest test-e2e

.PHONY: fmt-go
fmt-go:
$(GO_BUILD_ENV) go fmt ./...
Expand Down
21 changes: 21 additions & 0 deletions api/policies/v1/factories.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,9 @@ type PolicyServerBuilder struct {
requests corev1.ResourceList
sigstoreTrustConfigMap string
namespacedPoliciesCapabilities []string
webhookPort *int32
readinessProbePort *int32
metricsPort *int32
}

func NewPolicyServerFactory() *PolicyServerBuilder {
Expand Down Expand Up @@ -535,6 +538,21 @@ func (f *PolicyServerBuilder) WithNamespacedPoliciesCapabilities(capabilities []
return f
}

func (f *PolicyServerBuilder) WithWebhookPort(port int32) *PolicyServerBuilder {
f.webhookPort = &port
return f
}

func (f *PolicyServerBuilder) WithReadinessProbePort(port int32) *PolicyServerBuilder {
f.readinessProbePort = &port
return f
}

func (f *PolicyServerBuilder) WithMetricsPort(port int32) *PolicyServerBuilder {
f.metricsPort = &port
return f
}

func (f *PolicyServerBuilder) Build() *PolicyServer {
policyServer := PolicyServer{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -559,6 +577,9 @@ func (f *PolicyServerBuilder) Build() *PolicyServer {
Requests: f.requests,
SigstoreTrustConfig: f.sigstoreTrustConfigMap,
NamespacedPoliciesCapabilities: f.namespacedPoliciesCapabilities,
WebhookPort: f.webhookPort,
ReadinessProbePort: f.readinessProbePort,
MetricsPort: f.metricsPort,
Env: []corev1.EnvVar{
{
Name: "KUBEWARDEN_LOG_LEVEL",
Expand Down
50 changes: 50 additions & 0 deletions api/policies/v1/policyserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,28 @@ type PolicyServerSpec struct {
// - Specific capability paths (e.g. "oci/v1/verify", "net/v1/dns_lookup_host")
// +optional
NamespacedPoliciesCapabilities []string `json:"namespacedPoliciesCapabilities,omitempty"`

// Port where the policy server listens for incoming webhook requests.
// When unset, defaults to 8443. This is the port the Kubernetes API server
// reaches when evaluating admission requests.
// +optional
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
WebhookPort *int32 `json:"webhookPort,omitempty"`

// Port used by the policy server to expose the readiness probe endpoint.
// When unset, defaults to 8081.
// +optional
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
ReadinessProbePort *int32 `json:"readinessProbePort,omitempty"`

// Port used by the policy server to expose the metrics endpoint.
// When unset, defaults to 8080. Only relevant when metrics are enabled.
// +optional
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
MetricsPort *int32 `json:"metricsPort,omitempty"`
}

type ReconciliationTransitionReason string
Expand Down Expand Up @@ -222,6 +244,34 @@ func (ps *PolicyServer) AppLabel() string {
return "kubewarden-" + ps.NameWithPrefix()
}

// EffectiveWebhookPort returns the port the policy server listens on for
// admission webhook requests, using the CRD field when set or the default constant.
func (ps *PolicyServer) EffectiveWebhookPort() int32 {
if ps.Spec.WebhookPort != nil {
return *ps.Spec.WebhookPort
}
return constants.PolicyServerListenPort
}

// EffectiveReadinessProbePort returns the port used for the readiness probe,
// using the CRD field when set or the default constant.
func (ps *PolicyServer) EffectiveReadinessProbePort() int32 {
if ps.Spec.ReadinessProbePort != nil {
return *ps.Spec.ReadinessProbePort
}
return constants.PolicyServerReadinessProbePort
}

// EffectiveMetricsPort returns the port used to expose the metrics endpoint.
// It returns the CRD-level override when set, otherwise it falls back to defaultPort
// (which is typically the controller-wide default configured via environment variable).
func (ps *PolicyServer) EffectiveMetricsPort(defaultPort int32) int32 {
if ps.Spec.MetricsPort != nil {
return *ps.Spec.MetricsPort
}
return defaultPort
}

// CommonLabels returns the common labels to be used with the resources
// associated to a Policy Server. The labels defined follow
// Kubernetes guidelines: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/#labels
Expand Down
51 changes: 47 additions & 4 deletions api/policies/v1/policyserver_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ var capabilityTree = capabilityNode{
}

// SetupWebhookWithManager registers the PolicyServer webhook with the controller manager.
func (ps *PolicyServer) SetupWebhookWithManager(mgr ctrl.Manager, deploymentsNamespace string) error {
// defaultMetricsPort is the controller-wide default metrics port (possibly overridden via env
// var) and is used by the validator to detect port conflicts involving the metrics port.
func (ps *PolicyServer) SetupWebhookWithManager(mgr ctrl.Manager, deploymentsNamespace string, defaultMetricsPort int32) error {
logger := mgr.GetLogger().WithName("policyserver-webhook")

err := ctrl.NewWebhookManagedBy(mgr, ps).
Expand All @@ -88,6 +90,7 @@ func (ps *PolicyServer) SetupWebhookWithManager(mgr ctrl.Manager, deploymentsNam
}).
WithValidator(&policyServerValidator{
deploymentsNamespace: deploymentsNamespace,
defaultMetricsPort: defaultMetricsPort,
k8sClient: mgr.GetClient(),
logger: logger,
}).
Expand Down Expand Up @@ -122,8 +125,12 @@ func (d *policyServerDefaulter) Default(_ context.Context, policyServer *PolicyS
// polyServerCustomValidator validates PolicyServers when they are created, updated, or deleted.
type policyServerValidator struct {
deploymentsNamespace string
k8sClient client.Client
logger logr.Logger
// defaultMetricsPort is the controller-wide default for the PolicyServer metrics port,
// which may be overridden via the KUBEWARDEN_POLICY_SERVER_SERVICES_METRICS_PORT env var.
// It is used when spec.metricsPort is not explicitly set on a PolicyServer.
defaultMetricsPort int32
k8sClient client.Client
logger logr.Logger
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
Expand Down Expand Up @@ -175,6 +182,7 @@ func (v *policyServerValidator) validate(ctx context.Context, policyServer *Poli

allErrs = append(allErrs, validateLimitsAndRequests(policyServer.Spec.Limits, policyServer.Spec.Requests)...)
allErrs = append(allErrs, validateNamespacedPoliciesCapabilities(policyServer.Spec.NamespacedPoliciesCapabilities)...)
allErrs = append(allErrs, v.validatePorts(policyServer)...)

if len(allErrs) == 0 {
return nil
Expand Down Expand Up @@ -274,7 +282,6 @@ func validateNamespacedPoliciesCapabilities(capabilities []string) field.ErrorLi
allErrs = append(allErrs, err)
}
}

return allErrs
}

Expand Down Expand Up @@ -327,3 +334,39 @@ func validateSingleCapability(pattern string, path *field.Path) *field.Error {
fmt.Sprintf("%q is not a complete capability path; use %q to allow all capabilities under it",
pattern, pattern+"/*"))
}

// validatePorts checks that the port fields in the PolicyServer spec do not
// conflict with each other. Effective ports (field value or controller-configured default)
// are compared so that partial overrides are also caught. The validator's defaultMetricsPort
// is used so that the env-var-configured controller default is taken into account.
func (v *policyServerValidator) validatePorts(policyServer *PolicyServer) field.ErrorList {
var allErrs field.ErrorList

webhookPort := policyServer.EffectiveWebhookPort()
readinessPort := policyServer.EffectiveReadinessProbePort()
metricsPort := policyServer.EffectiveMetricsPort(v.defaultMetricsPort)

Comment thread
jvanz marked this conversation as resolved.
if webhookPort == readinessPort {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec").Child("readinessProbePort"),
readinessPort,
fmt.Sprintf("readinessProbePort must differ from webhookPort (%d)", webhookPort),
))
}
if webhookPort == metricsPort {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec").Child("metricsPort"),
metricsPort,
fmt.Sprintf("metricsPort must differ from webhookPort (%d)", webhookPort),
))
}
if readinessPort == metricsPort {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec").Child("metricsPort"),
metricsPort,
fmt.Sprintf("metricsPort must differ from readinessProbePort (%d)", readinessPort),
Comment on lines +360 to +367
))
}

return allErrs
}
101 changes: 101 additions & 0 deletions api/policies/v1/policyserver_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,104 @@ func TestPolicyServerValidateNamespacedPoliciesCapabilities(t *testing.T) {
})
}
}

func TestValidatePorts(t *testing.T) {
tests := []struct {
name string
webhookPort *int32
readiness *int32
metrics *int32
defaultMetricsPort int32
errContains string
}{
{
name: "all defaults, no conflict",
defaultMetricsPort: constants.PolicyServerMetricsPort,
errContains: "",
},
{
name: "webhookPort equals readinessProbePort",
webhookPort: ptr.To[int32](8081),
readiness: ptr.To[int32](8081),
defaultMetricsPort: constants.PolicyServerMetricsPort,
errContains: "readinessProbePort must differ from webhookPort",
},
{
name: "webhookPort equals metricsPort",
webhookPort: ptr.To[int32](8080),
metrics: ptr.To[int32](8080),
defaultMetricsPort: constants.PolicyServerMetricsPort,
errContains: "metricsPort must differ from webhookPort",
},
{
name: "readinessProbePort equals metricsPort",
readiness: ptr.To[int32](9000),
metrics: ptr.To[int32](9000),
defaultMetricsPort: constants.PolicyServerMetricsPort,
errContains: "metricsPort must differ from readinessProbePort",
},
{
name: "all three ports the same",
webhookPort: ptr.To[int32](9999),
readiness: ptr.To[int32](9999),
metrics: ptr.To[int32](9999),
defaultMetricsPort: constants.PolicyServerMetricsPort,
errContains: "readinessProbePort must differ from webhookPort",
},
{
name: "all three ports distinct custom values",
webhookPort: ptr.To[int32](9443),
readiness: ptr.To[int32](9081),
metrics: ptr.To[int32](9080),
defaultMetricsPort: constants.PolicyServerMetricsPort,
errContains: "",
},
{
// When the controller default metrics port is overridden via env var to 9090,
// a PolicyServer with readinessProbePort=9090 (and no explicit metricsPort)
// must be rejected because the effective metrics port is 9090.
name: "readinessProbePort conflicts with env-var-configured default metrics port",
readiness: ptr.To[int32](9090),
defaultMetricsPort: 9090,
errContains: "metricsPort must differ from readinessProbePort",
},
{
// When the controller default metrics port is overridden via env var to 9090,
// a PolicyServer with no explicit port overrides should be valid.
name: "all defaults with non-standard env-var default metrics port, no conflict",
defaultMetricsPort: 9090,
errContains: "",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
k8sClient := fake.NewClientBuilder().Build()
builder := NewPolicyServerFactory()
if test.webhookPort != nil {
builder = builder.WithWebhookPort(*test.webhookPort)
}
if test.readiness != nil {
builder = builder.WithReadinessProbePort(*test.readiness)
}
if test.metrics != nil {
builder = builder.WithMetricsPort(*test.metrics)
}
policyServer := builder.Build()

validator := policyServerValidator{
deploymentsNamespace: "default",
defaultMetricsPort: test.defaultMetricsPort,
k8sClient: k8sClient,
logger: logr.Discard(),
}
err := validator.validate(t.Context(), policyServer)

if test.errContains != "" {
require.ErrorContains(t, err, test.errContains)
} else {
require.NoError(t, err)
}
})
}
}
30 changes: 30 additions & 0 deletions api/policies/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions charts/kubewarden-controller/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,20 @@ are configured.
- {{ .Values.auditScanner.reportCRDsKind }}
{{- end -}}
{{- end -}}

{{/*
Compute the effective affinity for the controller deployment.
Uses the controller-specific affinity if set, otherwise falls back to
global.affinity for backward compatibility.

NOTE: When hostNetwork is enabled, users are responsible for setting
appropriate podAntiAffinity rules to prevent host-port conflicts between
controller replicas on the same node.
*/}}
{{- define "kubewarden-controller.effectiveAffinity" -}}
{{- if .Values.affinity -}}
{{- toYaml .Values.affinity -}}
{{- else if .Values.global.affinity -}}
{{- toYaml .Values.global.affinity -}}
{{- end -}}
{{- end -}}
Loading
Loading