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
17 changes: 17 additions & 0 deletions api/v1beta1/serviceexport_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package v1beta1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
gatewayapi "sigs.k8s.io/gateway-api/apis/v1"
)

Expand Down Expand Up @@ -136,6 +137,7 @@ type ServiceExportStatus struct {
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.exportStatus`
// +kubebuilder:printcolumn:name="Alias",type=string,JSONPath=`.spec.aliases`
// +kubebuilder:resource:path=serviceexports,singular=serviceexport,shortName=svcex
// +kubebuilder:webhook:path=/validate-webhook,mutating=false,failurePolicy=fail,sideEffects=None,groups=networking.kubeslice.io,resources=serviceexports,verbs=create;update,versions=v1beta1,name=vserviceexport.networking.kubeslice.io,admissionReviewVersions=v1

// ServiceExport is the Schema for the serviceexports API
type ServiceExport struct {
Expand All @@ -158,3 +160,18 @@ type ServiceExportList struct {
func init() {
SchemeBuilder.Register(&ServiceExport{}, &ServiceExportList{})
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *ServiceExport) ValidateCreate() error {
return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *ServiceExport) ValidateUpdate(old runtime.Object) error {
return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *ServiceExport) ValidateDelete() error {
return nil
}
14 changes: 11 additions & 3 deletions config/samples/mesh_v1beta1_serviceexport.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
apiVersion: mesh.avesha.io/v1beta1
apiVersion: networking.kubeslice.io/v1beta1
kind: ServiceExport
metadata:
name: serviceexport-sample
namespace: app-namespace
spec:
# Add fields here
foo: bar
slice: test-slice
selector:
matchLabels:
app: nginx
ports:
- name: http
containerPort: 80
protocol: TCP
servicePort: 80
30 changes: 22 additions & 8 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ import (
"github.com/kubeslice/worker-operator/pkg/networkpolicy"
"github.com/kubeslice/worker-operator/pkg/utils"
podwh "github.com/kubeslice/worker-operator/pkg/webhook/pod"
svcexportwh "github.com/kubeslice/worker-operator/pkg/webhook/serviceexport"
unified "github.com/kubeslice/worker-operator/pkg/webhook"
//+kubebuilder:scaffold:imports
)

Expand Down Expand Up @@ -133,18 +135,30 @@ func main() {

// Use an environment variable to be able to disable webhooks, so that we can run the operator locally
if utils.GetEnvOrDefault("ENABLE_WEBHOOKS", "true") == "true" {
podWebhook := &podwh.WebhookServer{
Client: mgr.GetClient(),
SliceInfoClient: podwh.NewWebhookClient(),
Decoder: admission.NewDecoder(mgr.GetScheme()),
}
serviceExportWebhook := &svcexportwh.ServiceExportValidator{
Client: mgr.GetClient(),
Decoder: admission.NewDecoder(mgr.GetScheme()),
}

mgr.GetWebhookServer().Register("/mutate-webhook", &webhook.Admission{
Handler: &podwh.WebhookServer{
Client: mgr.GetClient(),
SliceInfoClient: podwh.NewWebhookClient(),
Decoder: admission.NewDecoder(mgr.GetScheme()),
Handler: &unified.UnifiedWebhookHandler{
Client: mgr.GetClient(),
Decoder: admission.NewDecoder(mgr.GetScheme()),
PodWebhook: podWebhook,
ServiceExportWebhook: serviceExportWebhook,
},
})
mgr.GetWebhookServer().Register("/validate-webhook", &webhook.Admission{
Handler: &podwh.WebhookServer{
Client: mgr.GetClient(),
SliceInfoClient: podwh.NewWebhookClient(),
Decoder: admission.NewDecoder(mgr.GetScheme()),
Handler: &unified.UnifiedWebhookHandler{
Client: mgr.GetClient(),
Decoder: admission.NewDecoder(mgr.GetScheme()),
PodWebhook: podWebhook,
ServiceExportWebhook: serviceExportWebhook,
},
})
}
Expand Down
85 changes: 85 additions & 0 deletions pkg/webhook/serviceexport/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2022 Avesha, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package serviceexport

import (
"context"
"fmt"
"net/http"

"github.com/kubeslice/worker-operator/api/v1beta1"
"github.com/kubeslice/worker-operator/pkg/logger"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

var log = logger.NewWrappedLogger().WithName("ServiceExportWebhook")

type ServiceExportValidator struct {
Client client.Client
Decoder *admission.Decoder
}

func (v *ServiceExportValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
svcExport := &v1beta1.ServiceExport{}
err := v.Decoder.Decode(req, svcExport)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

if req.Operation == "CREATE" || req.Operation == "UPDATE" {
if err := v.validateServiceExport(ctx, svcExport, req.Namespace); err != nil {
return admission.Denied(err.Error())
}
}

return admission.Allowed("")
}

func (v *ServiceExportValidator) validateServiceExport(ctx context.Context, svcExport *v1beta1.ServiceExport, namespace string) error {
sliceName := svcExport.Spec.Slice
if sliceName == "" {
return fmt.Errorf("slice name is required")
}

slice := &v1beta1.Slice{}
sliceKey := client.ObjectKey{Name: sliceName, Namespace: namespace}
if err := v.Client.Get(ctx, sliceKey, slice); err != nil {
return fmt.Errorf("slice '%s' not found on cluster: %v", sliceName, err)
}

if slice.Status.SliceConfig == nil || slice.Status.SliceConfig.NamespaceIsolationProfile == nil {
return fmt.Errorf("slice '%s' is not properly configured", sliceName)
}

appNamespaces := slice.Status.SliceConfig.NamespaceIsolationProfile.ApplicationNamespaces
namespaceFound := false
for _, appNs := range appNamespaces {
if appNs == namespace {
namespaceFound = true
break
}
}

if !namespaceFound {
return fmt.Errorf("namespace '%s' is not an onboarded application namespace for slice '%s'", namespace, sliceName)
}

return nil
}
128 changes: 128 additions & 0 deletions pkg/webhook/serviceexport/webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) 2022 Avesha, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package serviceexport

import (
"context"
"strings"
"testing"

"github.com/kubeslice/worker-operator/api/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestValidateServiceExport(t *testing.T) {
scheme := runtime.NewScheme()
_ = v1beta1.AddToScheme(scheme)

tests := []struct {
name string
svcExport *v1beta1.ServiceExport
slice *v1beta1.Slice
namespace string
shouldError bool
errorMsg string
}{
{
name: "valid serviceexport",
svcExport: &v1beta1.ServiceExport{
ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "app-ns"},
Spec: v1beta1.ServiceExportSpec{
Slice: "test-slice",
},
},
slice: &v1beta1.Slice{
ObjectMeta: metav1.ObjectMeta{Name: "test-slice", Namespace: "app-ns"},
Status: v1beta1.SliceStatus{
SliceConfig: &v1beta1.SliceConfig{
NamespaceIsolationProfile: &v1beta1.NamespaceIsolationProfile{
ApplicationNamespaces: []string{"app-ns"},
},
},
},
},
namespace: "app-ns",
shouldError: false,
},
{
name: "slice not found",
svcExport: &v1beta1.ServiceExport{
ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "app-ns"},
Spec: v1beta1.ServiceExportSpec{
Slice: "nonexistent-slice",
},
},
namespace: "app-ns",
shouldError: true,
errorMsg: "slice 'nonexistent-slice' not found on cluster",
},
{
name: "namespace not in application namespaces",
svcExport: &v1beta1.ServiceExport{
ObjectMeta: metav1.ObjectMeta{Name: "test-svc", Namespace: "wrong-ns"},
Spec: v1beta1.ServiceExportSpec{
Slice: "test-slice",
},
},
slice: &v1beta1.Slice{
ObjectMeta: metav1.ObjectMeta{Name: "test-slice", Namespace: "wrong-ns"},
Status: v1beta1.SliceStatus{
SliceConfig: &v1beta1.SliceConfig{
NamespaceIsolationProfile: &v1beta1.NamespaceIsolationProfile{
ApplicationNamespaces: []string{"app-ns"},
},
},
},
},
namespace: "wrong-ns",
shouldError: true,
errorMsg: "namespace 'wrong-ns' is not an onboarded application namespace for slice 'test-slice'",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var objs []runtime.Object
if tt.slice != nil {
objs = append(objs, tt.slice)
}

fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
validator := &ServiceExportValidator{
Client: fakeClient,
}

err := validator.validateServiceExport(context.Background(), tt.svcExport, tt.namespace)

if tt.shouldError {
if err == nil {
t.Errorf("expected error but got none")
} else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
t.Errorf("expected error message to contain '%s' but got '%s'", tt.errorMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
})
}
}
42 changes: 42 additions & 0 deletions pkg/webhook/unified.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2022 Avesha, Inc. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package webhook

import (
"context"

podwh "github.com/kubeslice/worker-operator/pkg/webhook/pod"
svcexportwh "github.com/kubeslice/worker-operator/pkg/webhook/serviceexport"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

type UnifiedWebhookHandler struct {
Client client.Client
Decoder *admission.Decoder
PodWebhook *podwh.WebhookServer
ServiceExportWebhook *svcexportwh.ServiceExportValidator
}

func (h *UnifiedWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response {
if req.Kind.Group == "networking.kubeslice.io" && req.Kind.Kind == "ServiceExport" {
return h.ServiceExportWebhook.Handle(ctx, req)
}
return h.PodWebhook.Handle(ctx, req)
}
Loading