generated from vshn/go-bootstrap
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replaces https://hub.syn.tools/appuio-cloud/references/policies/02_disallow_reserved_namespaces.html and https://hub.syn.tools/appuio-cloud/references/policies/02_validate_namespace_metadata.html Kyverno policies.
- Loading branch information
Showing
7 changed files
with
349 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package webhooks | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"slices" | ||
|
||
"go.uber.org/multierr" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"k8s.io/apimachinery/pkg/util/sets" | ||
"sigs.k8s.io/controller-runtime/pkg/log" | ||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission" | ||
"github.com/minio/pkg/wildcard" | ||
|
||
"github.com/appuio/appuio-cloud-agent/skipper" | ||
) | ||
|
||
// +kubebuilder:webhook:path=/validate-namespace-metadata,name=validate-namespace-metadata.appuio.io,admissionReviewVersions=v1,sideEffects=none,mutating=false,failurePolicy=Fail,groups="",resources=namespaces,verbs=create;update,versions=v1,matchPolicy=equivalent | ||
// +kubebuilder:webhook:path=/validate-namespace-metadata,name=validate-namespace-metadata-projectrequests.appuio.io,admissionReviewVersions=v1,sideEffects=none,mutating=false,failurePolicy=Fail,groups=project.openshift.io,resources=projectrequests,verbs=create;update,versions=v1,matchPolicy=equivalent | ||
|
||
// NamespaceMetadataValidator validates the metadata of a namespace. | ||
type NamespaceMetadataValidator struct { | ||
Decoder admission.Decoder | ||
|
||
Skipper skipper.Skipper | ||
|
||
// ReservedNamespace is a list of namespaces that are reserved and do not count towards the quota. | ||
// Supports '*' and '?' wildcards. | ||
ReservedNamespaces []string | ||
// AllowedAnnotations is a list of annotations that are allowed on the namespace. | ||
// Supports '*' and '?' wildcards. | ||
AllowedAnnotations []string | ||
// AllowedLabels is a list of labels that are allowed on the namespace. | ||
// Supports '*' and '?' wildcards. | ||
AllowedLabels []string | ||
} | ||
|
||
// Handle handles the admission requests | ||
func (v *NamespaceMetadataValidator) Handle(ctx context.Context, req admission.Request) admission.Response { | ||
ctx = log.IntoContext(ctx, log.FromContext(ctx). | ||
WithName("webhook.validate-namespace-metadata.appuio.io"). | ||
WithValues("id", req.UID, "user", req.UserInfo.Username). | ||
WithValues("namespace", req.Namespace, "name", req.Name, | ||
"group", req.Kind.Group, "version", req.Kind.Version, "kind", req.Kind.Kind)) | ||
|
||
return logAdmissionResponse(ctx, v.handle(ctx, req)) | ||
} | ||
|
||
func (v *NamespaceMetadataValidator) handle(ctx context.Context, req admission.Request) admission.Response { | ||
skip, err := v.Skipper.Skip(ctx, req) | ||
if err != nil { | ||
return admission.Errored(http.StatusInternalServerError, err) | ||
} | ||
if skip { | ||
return admission.Allowed("skipped") | ||
} | ||
|
||
for _, ns := range v.ReservedNamespaces { | ||
if wildcard.Match(ns, req.Name) { | ||
return admission.Denied("Changing or creating reserved namespaces is not allowed.") | ||
} | ||
} | ||
|
||
var oldObj unstructured.Unstructured | ||
if len(req.OldObject.Raw) > 0 { | ||
if err := v.Decoder.DecodeRaw(req.OldObject, &oldObj); err != nil { | ||
return admission.Errored(http.StatusBadRequest, fmt.Errorf("failed to decode old object: %w", err)) | ||
} | ||
} | ||
|
||
var newObj unstructured.Unstructured | ||
if err := v.Decoder.Decode(req, &newObj); err != nil { | ||
return admission.Errored(http.StatusBadRequest, fmt.Errorf("failed to decode object from request: %w", err)) | ||
} | ||
|
||
if err := validateChangedMap(oldObj.GetAnnotations(), newObj.GetAnnotations(), v.AllowedAnnotations, "annotation"); err != nil { | ||
return admission.Denied(formatDeniedMessage(err, "annotations", v.AllowedAnnotations, newObj.GetAnnotations(), oldObj.GetAnnotations())) | ||
} | ||
if err := validateChangedMap(oldObj.GetLabels(), newObj.GetLabels(), v.AllowedLabels, "label"); err != nil { | ||
return admission.Denied(formatDeniedMessage(err, "labels", v.AllowedLabels, newObj.GetLabels(), oldObj.GetLabels())) | ||
} | ||
|
||
return admission.Allowed("allowed") | ||
} | ||
|
||
func formatDeniedMessage(err error, errMapRef string, allowed []string, newMap, oldMap map[string]string) string { | ||
msg := `The request was denied: | ||
%v | ||
The following %s can be modified: | ||
%s | ||
%s given: | ||
%s | ||
%s before modification: | ||
%s | ||
` | ||
|
||
return fmt.Sprintf(msg, err, errMapRef, allowed, errMapRef, newMap, errMapRef, oldMap) | ||
} | ||
|
||
func validateChangedMap(old, new map[string]string, allowedKeys []string, errObjectRef string) error { | ||
changed := changedKeys(old, new) | ||
errs := make([]error, 0, len(changed)) | ||
for _, k := range changed { | ||
allowed := slices.ContainsFunc(allowedKeys, func(a string) bool { return wildcard.Match(a, k) }) | ||
if !allowed { | ||
errs = append(errs, fmt.Errorf("%s %q is not allowed to be changed", errObjectRef, k)) | ||
} | ||
} | ||
|
||
return multierr.Combine(errs...) | ||
} | ||
|
||
func changedKeys(a, b map[string]string) []string { | ||
changed := sets.New[string]() | ||
|
||
for k, v := range a { | ||
if bV, ok := b[k]; !ok || v != bV { | ||
changed.Insert(k) | ||
} | ||
} | ||
for k, v := range b { | ||
if aV, ok := a[k]; !ok || v != aV { | ||
changed.Insert(k) | ||
} | ||
} | ||
|
||
return sets.List(changed) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package webhooks | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
"github.com/appuio/appuio-cloud-agent/skipper" | ||
) | ||
|
||
func Test_NamespaceMetadataValidator_Handle(t *testing.T) { | ||
|
||
testCases := []struct { | ||
name string | ||
|
||
reservedNamespaces []string | ||
allowedAnnotations []string | ||
allowedLabels []string | ||
|
||
object client.Object | ||
oldObj client.Object | ||
|
||
allowed bool | ||
}{ | ||
{ | ||
name: "new namespace with allowed name", | ||
object: newNamespace("test-namespace", nil, nil), | ||
|
||
reservedNamespaces: []string{"appuio*"}, | ||
|
||
allowed: true, | ||
}, | ||
{ | ||
name: "new project with allowed name", | ||
object: newProjectRequest("test-project", nil, nil), | ||
|
||
reservedNamespaces: []string{"appuio*"}, | ||
|
||
allowed: true, | ||
}, | ||
{ | ||
name: "new namespace with reserved name", | ||
object: newNamespace("appuio-blub", nil, nil), | ||
|
||
reservedNamespaces: []string{"test", "appuio*"}, | ||
|
||
allowed: false, | ||
}, | ||
{ | ||
name: "new project with reserved name", | ||
object: newProjectRequest("appuio-blub", nil, nil), | ||
|
||
reservedNamespaces: []string{"test", "appuio*"}, | ||
|
||
allowed: false, | ||
}, | ||
{ | ||
name: "new namespace with allowed annotation", | ||
object: newNamespace("test-namespace", nil, map[string]string{"allowed": ""}), | ||
|
||
allowedAnnotations: []string{"allowed"}, | ||
allowed: true, | ||
}, | ||
{ | ||
name: "new namespace with disallowed annotation", | ||
object: newNamespace("test-namespace", nil, map[string]string{"disallowed": ""}), | ||
|
||
allowedAnnotations: []string{"allowed"}, | ||
allowed: false, | ||
}, | ||
{ | ||
name: "new namespace with allowed label", | ||
object: newNamespace("test-namespace", map[string]string{"allowed-kajshd": "", "custom/x": "asd"}, nil), | ||
|
||
allowedLabels: []string{"allowed*", "custom/*"}, | ||
allowed: true, | ||
}, | ||
{ | ||
name: "new namespace with disallowed label", | ||
object: newNamespace("test-namespace", map[string]string{"disallowed": ""}, nil), | ||
|
||
allowedLabels: []string{"allowed"}, | ||
allowed: false, | ||
}, | ||
{ | ||
name: "update namespace with allowed annotation", | ||
object: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s", "allowed": ""}), | ||
oldObj: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s"}), | ||
|
||
allowedAnnotations: []string{"allowed"}, | ||
allowed: true, | ||
}, | ||
{ | ||
name: "update namespace with disallowed annotation", | ||
object: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s", "disallowed": "a"}), | ||
oldObj: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s", "disallowed": "b"}), | ||
allowedAnnotations: []string{"allowed"}, | ||
allowed: false, | ||
}, | ||
{ | ||
name: "remove disallowed annotation", | ||
object: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s"}), | ||
oldObj: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s", "disallowed": "b", "disallowed2": "", "allowed": ""}), | ||
allowedAnnotations: []string{"allowed"}, | ||
allowed: false, | ||
}, | ||
{ | ||
name: "remove disallowed annotation", | ||
object: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s"}), | ||
oldObj: newNamespace("test-namespace", nil, map[string]string{"pre-existing": "s", "disallowed": ""}), | ||
allowedAnnotations: []string{"allowed"}, | ||
allowed: false, | ||
}, | ||
} | ||
|
||
_, scheme, decoder := prepareClient(t) | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
t.Parallel() | ||
|
||
subject := &NamespaceMetadataValidator{ | ||
Decoder: decoder, | ||
Skipper: skipper.StaticSkipper{}, | ||
ReservedNamespaces: tc.reservedNamespaces, | ||
AllowedAnnotations: tc.allowedAnnotations, | ||
AllowedLabels: tc.allowedLabels, | ||
} | ||
|
||
amr := admissionRequestForObjectWithOldObject(t, tc.object, tc.oldObj, scheme) | ||
|
||
resp := subject.Handle(context.Background(), amr) | ||
t.Log("Response:", resp.Result.Reason, resp.Result.Message) | ||
require.Equal(t, tc.allowed, resp.Allowed) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters