mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-19 04:26:45 +00:00
184 lines
9.2 KiB
Go
184 lines
9.2 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package errors_test
|
|
|
|
import (
|
|
stderrors "errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/projectcapsule/capsule/pkg/api"
|
|
apierrors "github.com/projectcapsule/capsule/pkg/api/errors"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8sapierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
|
|
)
|
|
|
|
func TestAllowedValuesErrorMessages(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
allowed := api.SelectorAllowedListSpec{
|
|
AllowedListSpec: api.AllowedListSpec{
|
|
Exact: []string{"fast", "slow"},
|
|
Regex: "premium-.*",
|
|
},
|
|
LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"class": "gold"}},
|
|
}
|
|
|
|
got := apierrors.AllowedValuesErrorMessage(allowed, "prefix: ")
|
|
for _, want := range []string{
|
|
"prefix:",
|
|
"use one from the following list (fast, slow)",
|
|
"use one matching the following regex (premium-.*)",
|
|
"matching the label selector defined in the Tenant",
|
|
} {
|
|
if !strings.Contains(got, want) {
|
|
t.Fatalf("AllowedValuesErrorMessage() = %q, missing %q", got, want)
|
|
}
|
|
}
|
|
|
|
defaultMsg := apierrors.DefaultAllowedValuesErrorMessage(api.DefaultAllowedListSpec{SelectorAllowedListSpec: allowed}, "default: ")
|
|
if !strings.Contains(defaultMsg, "premium-.*") {
|
|
t.Fatalf("DefaultAllowedValuesErrorMessage() = %q, want regex detail", defaultMsg)
|
|
}
|
|
|
|
selectionMsg := apierrors.SelectionListWithDefaultErrorMessage(api.SelectionListWithDefaultSpec{
|
|
SelectionListWithSpec: api.SelectionListWithSpec{
|
|
LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"class": "gold"}},
|
|
},
|
|
}, "selection: ")
|
|
if !strings.Contains(selectionMsg, "matching the label selector defined in the Tenant") {
|
|
t.Fatalf("SelectionListWithDefaultErrorMessage() = %q", selectionMsg)
|
|
}
|
|
}
|
|
|
|
func TestErrorConstructors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
allowed := api.DefaultAllowedListSpec{SelectorAllowedListSpec: api.SelectorAllowedListSpec{
|
|
AllowedListSpec: api.AllowedListSpec{Exact: []string{"allowed"}, Regex: "allowed-.*"},
|
|
}}
|
|
selectorAllowed := api.SelectorAllowedListSpec{AllowedListSpec: api.AllowedListSpec{Exact: []string{"allowed"}}}
|
|
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want string
|
|
}{
|
|
{name: "custom quota", err: apierrors.NewCustomResourceQuotaError("pods.v1", 3), want: "pods.v1"},
|
|
{name: "device forbidden", err: apierrors.NewDeviceClassForbidden("gpu", selectorAllowed), want: "Device Class gpu is forbidden"},
|
|
{name: "device undefined", err: apierrors.NewDeviceClassUndefined(selectorAllowed), want: "Selected DeviceClass is forbidden"},
|
|
{name: "gateway class", err: apierrors.NewGatewayClassError("public", stderrors.New("missing")), want: "Failed to resolve Gateway Class public"},
|
|
{name: "gateway", err: apierrors.NewGatewayError(gatewayv1.ObjectName("gw"), stderrors.New("missing")), want: "Failed to resolve Gateway gw"},
|
|
{name: "gateway forbidden", err: apierrors.NewGatewayClassForbidden("public", allowed), want: "Gateway Class public is forbidden"},
|
|
{name: "gateway undefined", err: apierrors.NewGatewayClassUndefined(allowed), want: "No gateway Class is forbidden"},
|
|
{name: "ingress class", err: apierrors.NewIngressClassError("nginx", stderrors.New("missing")), want: "Failed to resolve Ingress Class nginx"},
|
|
{name: "ingress forbidden", err: apierrors.NewIngressClassForbidden("nginx", allowed), want: "Ingress Class nginx is forbidden"},
|
|
{name: "ingress collision", err: apierrors.NewIngressHostnameCollision("example.com"), want: "example.com is already used"},
|
|
{name: "empty ingress hostname", err: apierrors.NewEmptyIngressHostname(api.AllowedListSpec{Exact: []string{"example.com"}, Regex: ".*\\.example\\.com"}), want: "empty hostname is not allowed"},
|
|
{name: "ingress hostnames invalid", err: apierrors.NewIngressHostnamesNotValid([]string{"bad_host"}, api.AllowedListSpec{Exact: []string{"example.com"}}), want: "Hostnames [bad_host] are not valid"},
|
|
{name: "ingress undefined", err: apierrors.NewIngressClassUndefined(allowed), want: "No Ingress Class is forbidden"},
|
|
{name: "ingress not valid", err: apierrors.NewIngressClassNotValid("nginx", allowed), want: "Ingress Class nginx is forbidden"},
|
|
{name: "namespace quota", err: apierrors.NewNamespaceQuotaExceededError(), want: "Cannot exceed Namespace quota"},
|
|
{name: "node labels", err: apierrors.NewNodeLabelForbiddenError(&api.ForbiddenListSpec{Exact: []string{"node-role"}, Regex: "forbidden.*"}), want: "some labels are marked as forbidden"},
|
|
{name: "node annotations", err: apierrors.NewNodeAnnotationForbiddenError(&api.ForbiddenListSpec{Exact: []string{"internal"}}), want: "some annotations are marked as forbidden"},
|
|
{name: "priority class", err: apierrors.NewPriorityClassError("high", stderrors.New("missing")), want: "Failed to resolve Priority Class high"},
|
|
{name: "pod metadata", err: apierrors.NewNoPodMetadata("pod"), want: "Skipping labels sync for pod"},
|
|
{name: "missing registry", err: apierrors.NewMissingContainerRegistryError("nginx"), want: "missing repository"},
|
|
{name: "registry forbidden", err: apierrors.NewContainerRegistryForbidden("docker.io/nginx", api.AllowedListSpec{Exact: []string{"ghcr.io"}, Regex: "registry.*"}), want: "registry is forbidden"},
|
|
{name: "pull policy", err: apierrors.NewImagePullPolicyForbidden("Always", "app", []string{"IfNotPresent"}), want: "ImagePullPolicy Always"},
|
|
{name: "pod priority forbidden", err: apierrors.NewPodPriorityClassForbidden("high", allowed), want: "Pod Priority Class high is forbidden"},
|
|
{name: "pod runtime forbidden", err: apierrors.NewPodRuntimeClassForbidden("kata", allowed), want: "Pod Runtime Class kata is forbidden"},
|
|
{name: "services metadata", err: apierrors.NewNoServicesMetadata("service"), want: "Skipping labels sync for service"},
|
|
{name: "external service IP forbidden empty", err: apierrors.NewExternalServiceIPForbidden(nil), want: "does not allow the use of Service with external IPs"},
|
|
{name: "external service IP forbidden cidr", err: apierrors.NewExternalServiceIPForbidden([]api.AllowedIP{"10.0.0.0/8"}), want: "10.0.0.0/8"},
|
|
{name: "nodeport disabled", err: apierrors.NewNodePortDisabledError(), want: "NodePort service types are forbidden"},
|
|
{name: "external name disabled", err: apierrors.NewExternalNameDisabledError(), want: "ExternalName service types are forbidden"},
|
|
{name: "loadbalancer disabled", err: apierrors.NewLoadBalancerDisabled(), want: "LoadBalancer service types are forbidden"},
|
|
{name: "storage class", err: apierrors.NewStorageClassError("fast", stderrors.New("missing")), want: "Failed to resolve Storage Class fast"},
|
|
{name: "storage class not valid", err: apierrors.NewStorageClassNotValid(allowed), want: "A valid Storage Class must be used"},
|
|
{name: "storage forbidden", err: apierrors.NewStorageClassForbidden("slow", allowed), want: "Storage Class slow is forbidden"},
|
|
{name: "tenant object", err: apierrors.NewNonTenantObject("obj"), want: "doesn't belong to tenant"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if got := tt.err.Error(); !strings.Contains(got, tt.want) {
|
|
t.Fatalf("Error() = %q, want substring %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEventedStorageErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []error{
|
|
apierrors.NewMissingTenantPVLabelsError("pv-a", events.ActionValidationDenied),
|
|
apierrors.NewCrossTenantPVMountError("pv-a", events.ActionValidationDenied),
|
|
apierrors.NewPVSelectorError(events.ActionValidationDenied),
|
|
apierrors.NewPvNotFoundError("pv-a", events.ActionValidationDenied),
|
|
}
|
|
|
|
for _, err := range tests {
|
|
evented, ok := err.(apierrors.EventedError)
|
|
if !ok {
|
|
t.Fatalf("%T does not implement EventedError", err)
|
|
}
|
|
if evented.Reason() != events.ReasonCrossTenantReference {
|
|
t.Fatalf("Reason() = %q, want %q", evented.Reason(), events.ReasonCrossTenantReference)
|
|
}
|
|
if evented.Action() != events.ActionValidationDenied {
|
|
t.Fatalf("Action() = %q, want %q", evented.Action(), events.ActionValidationDenied)
|
|
}
|
|
if err.Error() == "" {
|
|
t.Fatalf("Error() is empty")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMiscErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, err := range []error{
|
|
apierrors.RunningInOutOfClusterModeError{},
|
|
apierrors.CaNotYetValidError{},
|
|
apierrors.CaExpiredError{},
|
|
} {
|
|
if err.Error() == "" {
|
|
t.Fatalf("%T Error() is empty", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIgnoreGone(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if !apierrors.IgnoreGone(nil) {
|
|
t.Fatalf("IgnoreGone(nil) = false, want true")
|
|
}
|
|
if !apierrors.IgnoreGone(k8sapierrors.NewNotFound(schema.GroupResource{Group: "", Resource: "pods"}, "missing")) {
|
|
t.Fatalf("IgnoreGone(NotFound) = false, want true")
|
|
}
|
|
if !apierrors.IgnoreGone(&k8sapierrors.StatusError{ErrStatus: metav1.Status{
|
|
Reason: metav1.StatusReasonForbidden,
|
|
Details: &metav1.StatusDetails{Causes: []metav1.StatusCause{{
|
|
Type: corev1.NamespaceTerminatingCause,
|
|
}}},
|
|
}}) {
|
|
t.Fatalf("IgnoreGone(namespace terminating) = false, want true")
|
|
}
|
|
if !apierrors.IgnoreGone(stderrors.New("widget not found")) {
|
|
t.Fatalf("IgnoreGone(string not found) = false, want true")
|
|
}
|
|
if apierrors.IgnoreGone(stderrors.New("boom")) {
|
|
t.Fatalf("IgnoreGone(other error) = true, want false")
|
|
}
|
|
}
|