mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-05-08 10:17:17 +00:00
Merge pull request #67 from elgnay/csb-webhook
Add validating webhook for ManagedClusterSetBinding
This commit is contained in:
@@ -28,6 +28,7 @@ resources:
|
||||
- ./managedcluster.crd.yaml
|
||||
- ./manifestwork.crd.yaml
|
||||
- ./managedclusterset.crd.yaml
|
||||
- ./managedclustersetbinding.crd.yaml
|
||||
- ./namespace.yaml
|
||||
- ./service_account.yaml
|
||||
- ./hub_controller_clusterrole_binding.yaml
|
||||
|
||||
56
deploy/hub/managedclustersetbinding.crd.yaml
Normal file
56
deploy/hub/managedclustersetbinding.crd.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: managedclustersetbindings.cluster.open-cluster-management.io
|
||||
spec:
|
||||
group: cluster.open-cluster-management.io
|
||||
names:
|
||||
kind: ManagedClusterSetBinding
|
||||
listKind: ManagedClusterSetBindingList
|
||||
plural: managedclustersetbindings
|
||||
singular: managedclustersetbinding
|
||||
scope: Namespaced
|
||||
preserveUnknownFields: false
|
||||
validation:
|
||||
openAPIV3Schema:
|
||||
description: ManagedClusterSetBinding projects a ManagedClusterSet into a certain
|
||||
namespace. User is able to create a ManagedClusterSetBinding in a namespace
|
||||
and bind it to a ManagedClusterSet if they have an RBAC rule to GET on the
|
||||
virtual subresource of managedclustersets/bind. Workloads created in the same
|
||||
namespace can only be distributed to ManagedClusters in ManagedClustersets
|
||||
bound in this namespace by higher level controllers.
|
||||
type: object
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: Spec defines the attributes of ManagedClusterSetBinding.
|
||||
type: object
|
||||
properties:
|
||||
clusterSet:
|
||||
description: ClusterSet is the name of the ManagedClusterSet to bind.
|
||||
User is allowed to set or update this field if they have an RBAC rule
|
||||
to GET on the virtual subresource of managedclustersets/bind.
|
||||
type: string
|
||||
version: v1alpha1
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
@@ -53,3 +53,32 @@ webhooks:
|
||||
admissionReviewVersions: ["v1beta1"]
|
||||
sideEffects: None
|
||||
timeoutSeconds: 3
|
||||
|
||||
---
|
||||
|
||||
apiVersion: admissionregistration.k8s.io/v1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: managedclustersetbindingvalidators.admission.cluster.open-cluster-management.io
|
||||
webhooks:
|
||||
- name: managedclustersetbindingvalidators.admission.cluster.open-cluster-management.io
|
||||
failurePolicy: Fail
|
||||
clientConfig:
|
||||
service:
|
||||
# reach the webhook via the registered aggregated API
|
||||
namespace: default
|
||||
name: kubernetes
|
||||
path: /apis/admission.cluster.open-cluster-management.io/v1/managedclustersetbindingvalidators
|
||||
rules:
|
||||
- operations:
|
||||
- CREATE
|
||||
- UPDATE
|
||||
apiGroups:
|
||||
- cluster.open-cluster-management.io
|
||||
apiVersions:
|
||||
- "*"
|
||||
resources:
|
||||
- managedclustersetbindings
|
||||
admissionReviewVersions: ["v1beta1"]
|
||||
sideEffects: None
|
||||
timeoutSeconds: 3
|
||||
|
||||
@@ -3,14 +3,20 @@ package webhook
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/open-cluster-management/registration/pkg/webhook"
|
||||
clusterwebhook "github.com/open-cluster-management/registration/pkg/webhook/cluster"
|
||||
clustersetbindingwebhook "github.com/open-cluster-management/registration/pkg/webhook/clustersetbinding"
|
||||
admissionserver "github.com/openshift/generic-admission-server/pkg/cmd/server"
|
||||
"github.com/spf13/cobra"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
)
|
||||
|
||||
func NewAdmissionHook() *cobra.Command {
|
||||
o := admissionserver.NewAdmissionServerOptions(os.Stdout, os.Stderr, &webhook.ManagedClusterValidatingAdmissionHook{}, &webhook.ManagedClusterMutatingAdmissionHook{})
|
||||
o := admissionserver.NewAdmissionServerOptions(
|
||||
os.Stdout,
|
||||
os.Stderr,
|
||||
&clusterwebhook.ManagedClusterValidatingAdmissionHook{},
|
||||
&clusterwebhook.ManagedClusterMutatingAdmissionHook{},
|
||||
&clustersetbindingwebhook.ManagedClusterSetBindingValidatingAdmissionHook{})
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "webhook",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package webhook
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package webhook
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package webhook
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package webhook
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
130
pkg/webhook/clustersetbinding/validating_webhook.go
Normal file
130
pkg/webhook/clustersetbinding/validating_webhook.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package clustersetbinding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
clusterv1alpha1 "github.com/open-cluster-management/api/cluster/v1alpha1"
|
||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog"
|
||||
)
|
||||
|
||||
// ManagedClusterSetBindingValidatingAdmissionHook will validate the creating/updating ManagedClusterSetBinding request.
|
||||
type ManagedClusterSetBindingValidatingAdmissionHook struct {
|
||||
kubeClient kubernetes.Interface
|
||||
}
|
||||
|
||||
// ValidatingResource is called by generic-admission-server on startup to register the returned REST resource through which the
|
||||
// webhook is accessed by the kube apiserver.
|
||||
func (a *ManagedClusterSetBindingValidatingAdmissionHook) ValidatingResource() (plural schema.GroupVersionResource, singular string) {
|
||||
return schema.GroupVersionResource{
|
||||
Group: "admission.cluster.open-cluster-management.io",
|
||||
Version: "v1",
|
||||
Resource: "managedclustersetbindingvalidators",
|
||||
},
|
||||
"managedclustersetbindingvalidators"
|
||||
}
|
||||
|
||||
// Validate is called by generic-admission-server when the registered REST resource above is called with an admission request.
|
||||
func (a *ManagedClusterSetBindingValidatingAdmissionHook) Validate(admissionSpec *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
|
||||
klog.V(4).Infof("validate %q operation for object %q", admissionSpec.Operation, admissionSpec.Object)
|
||||
|
||||
// only validate the request for ManagedClusterSetBinding
|
||||
if admissionSpec.Resource.Group != "cluster.open-cluster-management.io" ||
|
||||
admissionSpec.Resource.Resource != "managedclustersetbindings" {
|
||||
return acceptRequest()
|
||||
}
|
||||
|
||||
// only handle Create/Update Operation
|
||||
if admissionSpec.Operation != admissionv1beta1.Create && admissionSpec.Operation != admissionv1beta1.Update {
|
||||
return acceptRequest()
|
||||
}
|
||||
|
||||
binding := &clusterv1alpha1.ManagedClusterSetBinding{}
|
||||
if err := json.Unmarshal(admissionSpec.Object.Raw, binding); err != nil {
|
||||
return denyRequest(http.StatusBadRequest, metav1.StatusReasonBadRequest,
|
||||
fmt.Sprintf("Unable to unmarshal the ManagedClusterSetBinding object: %v", err))
|
||||
}
|
||||
|
||||
// force the instance name to match the target cluster set name
|
||||
if binding.Name != binding.Spec.ClusterSet {
|
||||
return denyRequest(http.StatusBadRequest, metav1.StatusReasonBadRequest,
|
||||
"The ManagedClusterSetBinding must have the same name as the target ManagedClusterSet")
|
||||
}
|
||||
|
||||
// check if the request user has permission to bind the target cluster set
|
||||
if admissionSpec.Operation == admissionv1beta1.Create {
|
||||
return a.allowBindingToClusterSet(binding.Spec.ClusterSet, admissionSpec.UserInfo)
|
||||
}
|
||||
|
||||
return acceptRequest()
|
||||
}
|
||||
|
||||
// Initialize is called by generic-admission-server on startup to setup initialization that ManagedClusterSetBinding webhook needs.
|
||||
func (a *ManagedClusterSetBindingValidatingAdmissionHook) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
|
||||
var err error
|
||||
a.kubeClient, err = kubernetes.NewForConfig(kubeClientConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// allowBindingToClusterSet checks if the user has permission to bind a particular cluster set
|
||||
func (a *ManagedClusterSetBindingValidatingAdmissionHook) allowBindingToClusterSet(clusterSetName string, userInfo authenticationv1.UserInfo) *admissionv1beta1.AdmissionResponse {
|
||||
extra := make(map[string]authorizationv1.ExtraValue)
|
||||
for k, v := range userInfo.Extra {
|
||||
extra[k] = authorizationv1.ExtraValue(v)
|
||||
}
|
||||
|
||||
sar := &authorizationv1.SubjectAccessReview{
|
||||
Spec: authorizationv1.SubjectAccessReviewSpec{
|
||||
User: userInfo.Username,
|
||||
UID: userInfo.UID,
|
||||
Groups: userInfo.Groups,
|
||||
Extra: extra,
|
||||
ResourceAttributes: &authorizationv1.ResourceAttributes{
|
||||
Group: "cluster.open-cluster-management.io",
|
||||
Resource: "managedclustersets",
|
||||
Subresource: "bind",
|
||||
Verb: "create",
|
||||
Name: clusterSetName,
|
||||
},
|
||||
},
|
||||
}
|
||||
sar, err := a.kubeClient.AuthorizationV1().SubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return denyRequest(http.StatusForbidden, metav1.StatusReasonForbidden, err.Error())
|
||||
}
|
||||
if !sar.Status.Allowed {
|
||||
return denyRequest(http.StatusForbidden, metav1.StatusReasonForbidden, fmt.Sprintf("user %q is not allowed to bind cluster set %q", userInfo.Username, clusterSetName))
|
||||
}
|
||||
return acceptRequest()
|
||||
}
|
||||
|
||||
func acceptRequest() *admissionv1beta1.AdmissionResponse {
|
||||
return &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
func denyRequest(code int32, reason metav1.StatusReason, message string) *admissionv1beta1.AdmissionResponse {
|
||||
return &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Code: code,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
171
pkg/webhook/clustersetbinding/validating_webhook_test.go
Normal file
171
pkg/webhook/clustersetbinding/validating_webhook_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package clustersetbinding
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
clusterv1alpha1 "github.com/open-cluster-management/api/cluster/v1alpha1"
|
||||
admissionv1beta1 "k8s.io/api/admission/v1beta1"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
clienttesting "k8s.io/client-go/testing"
|
||||
)
|
||||
|
||||
var managedclustersetbindingSchema = metav1.GroupVersionResource{
|
||||
Group: "cluster.open-cluster-management.io",
|
||||
Version: "v1alpha1",
|
||||
Resource: "managedclustersetbindings",
|
||||
}
|
||||
|
||||
func TestManagedClusterValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
request *admissionv1beta1.AdmissionRequest
|
||||
expectedResponse *admissionv1beta1.AdmissionResponse
|
||||
allowBindingToClusterSet bool
|
||||
}{
|
||||
{
|
||||
name: "validate non-managedclustersetbindings request",
|
||||
request: &admissionv1beta1.AdmissionRequest{
|
||||
Resource: metav1.GroupVersionResource{
|
||||
Group: "test.open-cluster-management.io",
|
||||
Version: "v1",
|
||||
Resource: "tests",
|
||||
},
|
||||
},
|
||||
expectedResponse: &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validate deleting operation",
|
||||
request: &admissionv1beta1.AdmissionRequest{
|
||||
Resource: managedclustersetbindingSchema,
|
||||
Operation: admissionv1beta1.Delete,
|
||||
},
|
||||
expectedResponse: &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validate creating cluster set binding",
|
||||
request: &admissionv1beta1.AdmissionRequest{
|
||||
Resource: managedclustersetbindingSchema,
|
||||
Operation: admissionv1beta1.Create,
|
||||
Object: newManagedClusterSetBindingObj("ns1", "cs1", "cs1", nil),
|
||||
},
|
||||
allowBindingToClusterSet: true,
|
||||
expectedResponse: &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validate creating cluster set binding with unmatched name",
|
||||
request: &admissionv1beta1.AdmissionRequest{
|
||||
Resource: managedclustersetbindingSchema,
|
||||
Operation: admissionv1beta1.Create,
|
||||
Object: newManagedClusterSetBindingObj("ns1", "csb1", "cs1", nil),
|
||||
},
|
||||
expectedResponse: &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
|
||||
Message: "The ManagedClusterSetBinding must have the same name as the target ManagedClusterSet",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validate creating cluster set binding without permission",
|
||||
request: &admissionv1beta1.AdmissionRequest{
|
||||
Resource: managedclustersetbindingSchema,
|
||||
Operation: admissionv1beta1.Create,
|
||||
Object: newManagedClusterSetBindingObj("ns1", "cs1", "cs1", nil),
|
||||
},
|
||||
expectedResponse: &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: metav1.StatusFailure, Code: http.StatusForbidden, Reason: metav1.StatusReasonForbidden,
|
||||
Message: "user \"\" is not allowed to bind cluster set \"cs1\"",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validate updating cluster set binding",
|
||||
request: &admissionv1beta1.AdmissionRequest{
|
||||
Resource: managedclustersetbindingSchema,
|
||||
Operation: admissionv1beta1.Update,
|
||||
Object: newManagedClusterSetBindingObj("ns1", "cs1", "cs1", nil),
|
||||
OldObject: newManagedClusterSetBindingObj("ns1", "cs1", "cs2", map[string]string{
|
||||
"team": "team1",
|
||||
}),
|
||||
},
|
||||
allowBindingToClusterSet: true,
|
||||
expectedResponse: &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "validate updating cluster set binding with different cluster set",
|
||||
request: &admissionv1beta1.AdmissionRequest{
|
||||
Resource: managedclustersetbindingSchema,
|
||||
Operation: admissionv1beta1.Update,
|
||||
Object: newManagedClusterSetBindingObj("ns1", "cs1", "cs2", nil),
|
||||
OldObject: newManagedClusterSetBindingObj("ns1", "cs1", "cs1", nil),
|
||||
},
|
||||
expectedResponse: &admissionv1beta1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: metav1.StatusFailure, Code: http.StatusBadRequest, Reason: metav1.StatusReasonBadRequest,
|
||||
Message: "The ManagedClusterSetBinding must have the same name as the target ManagedClusterSet",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
kubeClient := kubefake.NewSimpleClientset()
|
||||
kubeClient.PrependReactor(
|
||||
"create",
|
||||
"subjectaccessreviews",
|
||||
func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, &authorizationv1.SubjectAccessReview{
|
||||
Status: authorizationv1.SubjectAccessReviewStatus{
|
||||
Allowed: c.allowBindingToClusterSet,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
|
||||
admissionHook := &ManagedClusterSetBindingValidatingAdmissionHook{
|
||||
kubeClient: kubeClient,
|
||||
}
|
||||
|
||||
actualResponse := admissionHook.Validate(c.request)
|
||||
if !reflect.DeepEqual(actualResponse, c.expectedResponse) {
|
||||
t.Errorf("expected %#v but got: %#v", c.expectedResponse.Result, actualResponse.Result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newManagedClusterSetBindingObj(namespace, name, clusterSetName string, labels map[string]string) runtime.RawExtension {
|
||||
managedClusterSetBinding := &clusterv1alpha1.ManagedClusterSetBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
Labels: labels,
|
||||
},
|
||||
Spec: clusterv1alpha1.ManagedClusterSetBindingSpec{
|
||||
ClusterSet: clusterSetName,
|
||||
},
|
||||
}
|
||||
bindingObj, _ := json.Marshal(managedClusterSetBinding)
|
||||
return runtime.RawExtension{
|
||||
Raw: bindingObj,
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
clusterv1client "github.com/open-cluster-management/api/client/cluster/clientset/versioned"
|
||||
clusterv1 "github.com/open-cluster-management/api/cluster/v1"
|
||||
clusterv1alpha1 "github.com/open-cluster-management/api/cluster/v1alpha1"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -25,13 +26,14 @@ import (
|
||||
|
||||
const (
|
||||
apiserviceName = "v1.admission.cluster.open-cluster-management.io"
|
||||
admissionName = "managedclustervalidators.admission.cluster.open-cluster-management.io"
|
||||
invalidURL = "127.0.0.1:8001"
|
||||
validURL = "https://127.0.0.1:8443"
|
||||
saNamespace = "default"
|
||||
)
|
||||
|
||||
var _ = ginkgo.Describe("Managed cluster admission hook", func() {
|
||||
var _ = ginkgo.Describe("Admission webhook", func() {
|
||||
var admissionName string
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
// make sure the api service v1.admission.cluster.open-cluster-management.io is available
|
||||
gomega.Eventually(func() bool {
|
||||
@@ -45,145 +47,312 @@ var _ = ginkgo.Describe("Managed cluster admission hook", func() {
|
||||
return apiService.Status.Conditions[0].Type == apiregistrationv1.Available &&
|
||||
apiService.Status.Conditions[0].Status == apiregistrationv1.ConditionTrue
|
||||
}, 60*time.Second, 1*time.Second).Should(gomega.BeTrue())
|
||||
// make sure the managedcluster can be created successfully
|
||||
gomega.Eventually(func() bool {
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
managedCluster := newManagedCluster(clusterName, false, validURL)
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
clusterClient.ClusterV1().ManagedClusters().Delete(context.TODO(), clusterName, metav1.DeleteOptions{})
|
||||
return true
|
||||
}, 60*time.Second, 1*time.Second).Should(gomega.BeTrue())
|
||||
})
|
||||
|
||||
ginkgo.Context("Creating a managed cluster", func() {
|
||||
ginkgo.It("Should have the default LeaseDurationSeconds", func() {
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
ginkgo.By(fmt.Sprintf("create a managed cluster %q", clusterName))
|
||||
ginkgo.Context("ManagedCluster", func() {
|
||||
ginkgo.BeforeEach(func() {
|
||||
admissionName = "managedclustervalidators.admission.cluster.open-cluster-management.io"
|
||||
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), newManagedCluster(clusterName, false, validURL), metav1.CreateOptions{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
gomega.Expect(managedCluster.Spec.LeaseDurationSeconds).To(gomega.Equal(int32(60)))
|
||||
// make sure the managedcluster can be created successfully
|
||||
gomega.Eventually(func() bool {
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
managedCluster := newManagedCluster(clusterName, false, validURL)
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
clusterClient.ClusterV1().ManagedClusters().Delete(context.TODO(), clusterName, metav1.DeleteOptions{})
|
||||
return true
|
||||
}, 60*time.Second, 1*time.Second).Should(gomega.BeTrue())
|
||||
})
|
||||
|
||||
ginkgo.It("Should respond bad request when creating a managed cluster with invalid external server URLs", func() {
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
ginkgo.By(fmt.Sprintf("create a managed cluster %q with an invalid external server URL %q", clusterName, invalidURL))
|
||||
ginkgo.Context("Creating a managed cluster", func() {
|
||||
ginkgo.It("Should have the default LeaseDurationSeconds", func() {
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
ginkgo.By(fmt.Sprintf("create a managed cluster %q", clusterName))
|
||||
|
||||
managedCluster := newManagedCluster(clusterName, false, invalidURL)
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), newManagedCluster(clusterName, false, validURL), metav1.CreateOptions{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsBadRequest(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: url \"%s\" is invalid in client configs",
|
||||
admissionName,
|
||||
invalidURL,
|
||||
)))
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
gomega.Expect(managedCluster.Spec.LeaseDurationSeconds).To(gomega.Equal(int32(60)))
|
||||
})
|
||||
|
||||
ginkgo.It("Should respond bad request when creating a managed cluster with invalid external server URLs", func() {
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
ginkgo.By(fmt.Sprintf("create a managed cluster %q with an invalid external server URL %q", clusterName, invalidURL))
|
||||
|
||||
managedCluster := newManagedCluster(clusterName, false, invalidURL)
|
||||
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsBadRequest(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: url \"%s\" is invalid in client configs",
|
||||
admissionName,
|
||||
invalidURL,
|
||||
)))
|
||||
})
|
||||
|
||||
ginkgo.It("Should forbid the request when creating an accepted managed cluster by unauthorized user", func() {
|
||||
sa := fmt.Sprintf("webhook-sa-%s", rand.String(6))
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
|
||||
ginkgo.By(fmt.Sprintf("create an managed cluster %q with unauthorized service account %q", clusterName, sa))
|
||||
|
||||
// prepare an unauthorized cluster client from a service account who can create/get/update ManagedCluster
|
||||
// but cannot change the ManagedCluster HubAcceptsClient field
|
||||
unauthorizedClient, err := buildClusterClient(saNamespace, sa, []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclusters"},
|
||||
Verbs: []string{"create", "get", "update"},
|
||||
},
|
||||
}, nil)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
managedCluster := newManagedCluster(clusterName, true, validURL)
|
||||
|
||||
_, err = unauthorizedClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsForbidden(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: user \"system:serviceaccount:%s:%s\" cannot update the HubAcceptsClient field",
|
||||
admissionName,
|
||||
saNamespace,
|
||||
sa,
|
||||
)))
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.It("Should forbid the request when creating an accepted managed cluster by unauthorized user", func() {
|
||||
sa := fmt.Sprintf("webhook-sa-%s", rand.String(6))
|
||||
clusterName := fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
ginkgo.Context("Updating a managed cluster", func() {
|
||||
var clusterName string
|
||||
|
||||
ginkgo.By(fmt.Sprintf("create an managed cluster %q with unauthorized service account %q", clusterName, sa))
|
||||
ginkgo.BeforeEach(func() {
|
||||
clusterName = fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
managedCluster := newManagedCluster(clusterName, false, validURL)
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
|
||||
// prepare an unauthorized cluster client from a service account who can create/get/update ManagedCluster
|
||||
// but cannot change the ManagedCluster HubAcceptsClient field
|
||||
unauthorizedClient, err := buildUnauthorizedClusterClient(sa)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
ginkgo.It("Should not update the LeaseDurationSeconds to zero", func() {
|
||||
ginkgo.By(fmt.Sprintf("try to update managed cluster %q LeaseDurationSeconds to zero", clusterName))
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
managedCluster := newManagedCluster(clusterName, true, validURL)
|
||||
managedCluster.Spec.LeaseDurationSeconds = 0
|
||||
_, err = clusterClient.ClusterV1().ManagedClusters().Update(context.TODO(), managedCluster, metav1.UpdateOptions{})
|
||||
return err
|
||||
})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
_, err = unauthorizedClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsForbidden(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: user \"system:serviceaccount:%s:%s\" cannot update the HubAcceptsClient field",
|
||||
admissionName,
|
||||
saNamespace,
|
||||
sa,
|
||||
)))
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
gomega.Expect(managedCluster.Spec.LeaseDurationSeconds).To(gomega.Equal(int32(60)))
|
||||
})
|
||||
|
||||
ginkgo.It("Should respond bad request when updating a managed cluster with invalid external server URLs", func() {
|
||||
ginkgo.By(fmt.Sprintf("update managed cluster %q with an invalid external server URL %q", clusterName, invalidURL))
|
||||
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
managedCluster.Spec.ManagedClusterClientConfigs[0].URL = invalidURL
|
||||
_, err = clusterClient.ClusterV1().ManagedClusters().Update(context.TODO(), managedCluster, metav1.UpdateOptions{})
|
||||
return err
|
||||
})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsBadRequest(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: url \"%s\" is invalid in client configs",
|
||||
admissionName,
|
||||
invalidURL,
|
||||
)))
|
||||
})
|
||||
|
||||
ginkgo.It("Should forbid the request when updating an unaccepted managed cluster to accepted by unauthorized user", func() {
|
||||
sa := fmt.Sprintf("webhook-sa-%s", rand.String(6))
|
||||
ginkgo.By(fmt.Sprintf("accept managed cluster %q by an unauthorized user %q", clusterName, sa))
|
||||
|
||||
// prepare an unauthorized cluster client from a service account who can create/get/update ManagedCluster
|
||||
// but cannot change the ManagedCluster HubAcceptsClient field
|
||||
unauthorizedClient, err := buildClusterClient(saNamespace, sa, []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclusters"},
|
||||
Verbs: []string{"create", "get", "update"},
|
||||
},
|
||||
}, nil)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
managedCluster, err := unauthorizedClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
managedCluster.Spec.HubAcceptsClient = true
|
||||
_, err = unauthorizedClient.ClusterV1().ManagedClusters().Update(context.TODO(), managedCluster, metav1.UpdateOptions{})
|
||||
return err
|
||||
})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsForbidden(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: user \"system:serviceaccount:%s:%s\" cannot update the HubAcceptsClient field",
|
||||
admissionName,
|
||||
saNamespace,
|
||||
sa,
|
||||
)))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
ginkgo.Context("Updating a managed cluster", func() {
|
||||
var clusterName string
|
||||
ginkgo.Context("ManagedClusterSetBinding", func() {
|
||||
var namespace string
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
clusterName = fmt.Sprintf("webhook-spoke-%s", rand.String(6))
|
||||
managedCluster := newManagedCluster(clusterName, false, validURL)
|
||||
_, err := clusterClient.ClusterV1().ManagedClusters().Create(context.TODO(), managedCluster, metav1.CreateOptions{})
|
||||
admissionName = "managedclustersetbindingvalidators.admission.cluster.open-cluster-management.io"
|
||||
|
||||
// create a namespace for testing
|
||||
namespace = fmt.Sprintf("ns-%s", rand.String(6))
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace,
|
||||
},
|
||||
}
|
||||
_, err := hubClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
// make sure the managedclusterset can be created successfully
|
||||
gomega.Eventually(func() bool {
|
||||
clusterSetName := fmt.Sprintf("clusterset-%s", rand.String(6))
|
||||
managedClusterSetBinding := newManagedClusterSetBinding(namespace, clusterSetName, clusterSetName)
|
||||
_, err := clusterClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Create(context.TODO(), managedClusterSetBinding, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
clusterClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Delete(context.TODO(), clusterSetName, metav1.DeleteOptions{})
|
||||
return true
|
||||
}, 60*time.Second, 1*time.Second).Should(gomega.BeTrue())
|
||||
})
|
||||
|
||||
ginkgo.It("Should not update the LeaseDurationSeconds to zero", func() {
|
||||
ginkgo.By(fmt.Sprintf("try to update managed cluster %q LeaseDurationSeconds to zero", clusterName))
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
managedCluster.Spec.LeaseDurationSeconds = 0
|
||||
_, err = clusterClient.ClusterV1().ManagedClusters().Update(context.TODO(), managedCluster, metav1.UpdateOptions{})
|
||||
return err
|
||||
})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
gomega.Expect(managedCluster.Spec.LeaseDurationSeconds).To(gomega.Equal(int32(60)))
|
||||
ginkgo.AfterEach(func() {
|
||||
hubClient.CoreV1().Namespaces().Delete(context.TODO(), namespace, metav1.DeleteOptions{})
|
||||
})
|
||||
|
||||
ginkgo.It("Should respond bad request when updating a managed cluster with invalid external server URLs", func() {
|
||||
ginkgo.By(fmt.Sprintf("update managed cluster %q with an invalid external server URL %q", clusterName, invalidURL))
|
||||
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
managedCluster, err := clusterClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
managedCluster.Spec.ManagedClusterClientConfigs[0].URL = invalidURL
|
||||
_, err = clusterClient.ClusterV1().ManagedClusters().Update(context.TODO(), managedCluster, metav1.UpdateOptions{})
|
||||
return err
|
||||
ginkgo.Context("Creating a ManagedClusterSetBinding", func() {
|
||||
ginkgo.It("should deny the request when creating a ManagedClusterSetBinding with unmatched cluster set name", func() {
|
||||
clusterSetName := fmt.Sprintf("clusterset-%s", rand.String(6))
|
||||
clusterSetBindingName := fmt.Sprintf("clustersetbinding-%s", rand.String(6))
|
||||
managedClusterSetBinding := newManagedClusterSetBinding(namespace, clusterSetBindingName, clusterSetName)
|
||||
_, err := clusterClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Create(context.TODO(), managedClusterSetBinding, metav1.CreateOptions{})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsBadRequest(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: The ManagedClusterSetBinding must have the same name as the target ManagedClusterSet",
|
||||
admissionName,
|
||||
)))
|
||||
})
|
||||
|
||||
ginkgo.It("should accept the request when creating a ManagedClusterSetBinding by authorized user", func() {
|
||||
sa := fmt.Sprintf("webhook-sa-%s", rand.String(6))
|
||||
clusterSetName := fmt.Sprintf("clusterset-%s", rand.String(6))
|
||||
|
||||
authorizedClient, err := buildClusterClient(namespace, sa, []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclustersets/bind"},
|
||||
Verbs: []string{"create"},
|
||||
},
|
||||
}, []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclustersetbindings"},
|
||||
Verbs: []string{"create", "get", "update"},
|
||||
},
|
||||
})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
managedClusterSetBinding := newManagedClusterSetBinding(namespace, clusterSetName, clusterSetName)
|
||||
_, err = authorizedClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Create(context.TODO(), managedClusterSetBinding, metav1.CreateOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
|
||||
ginkgo.It("should forbid the request when creating a ManagedClusterSetBinding by unauthorized user", func() {
|
||||
sa := fmt.Sprintf("webhook-sa-%s", rand.String(6))
|
||||
clusterSetName := fmt.Sprintf("clusterset-%s", rand.String(6))
|
||||
|
||||
// prepare an unauthorized cluster client from a service account who can create/get/update ManagedClusterSetBinding
|
||||
// but cannot bind ManagedClusterSet
|
||||
unauthorizedClient, err := buildClusterClient(namespace, sa, nil, []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclustersetbindings"},
|
||||
Verbs: []string{"create", "get", "update"},
|
||||
},
|
||||
})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
managedClusterSetBinding := newManagedClusterSetBinding(namespace, clusterSetName, clusterSetName)
|
||||
_, err = unauthorizedClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Create(context.TODO(), managedClusterSetBinding, metav1.CreateOptions{})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsForbidden(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: user \"system:serviceaccount:%s:%s\" is not allowed to bind cluster set \"%s\"",
|
||||
admissionName,
|
||||
namespace,
|
||||
sa,
|
||||
clusterSetName,
|
||||
)))
|
||||
})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsBadRequest(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: url \"%s\" is invalid in client configs",
|
||||
admissionName,
|
||||
invalidURL,
|
||||
)))
|
||||
})
|
||||
|
||||
ginkgo.It("Should forbid the request when updating an unaccepted managed cluster to accepted by unauthorized user", func() {
|
||||
sa := fmt.Sprintf("webhook-sa-%s", rand.String(6))
|
||||
ginkgo.By(fmt.Sprintf("accept managed cluster %q by an unauthorized user %q", clusterName, sa))
|
||||
|
||||
// prepare an unauthorized cluster client from a service account who can create/get/update ManagedCluster
|
||||
// but cannot change the ManagedCluster HubAcceptsClient field
|
||||
unauthorizedClient, err := buildUnauthorizedClusterClient(sa)
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
managedCluster, err := unauthorizedClient.ClusterV1().ManagedClusters().Get(context.TODO(), clusterName, metav1.GetOptions{})
|
||||
ginkgo.Context("Updating a ManagedClusterSetBinding", func() {
|
||||
ginkgo.It("should deny the request when updating a ManagedClusterSetBinding with a new cluster set", func() {
|
||||
// create a cluster set binding
|
||||
clusterSetName := fmt.Sprintf("clusterset-%s", rand.String(6))
|
||||
managedClusterSetBinding := newManagedClusterSetBinding(namespace, clusterSetName, clusterSetName)
|
||||
managedClusterSetBinding, err := clusterClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Create(context.TODO(), managedClusterSetBinding, metav1.CreateOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
managedCluster.Spec.HubAcceptsClient = true
|
||||
_, err = unauthorizedClient.ClusterV1().ManagedClusters().Update(context.TODO(), managedCluster, metav1.UpdateOptions{})
|
||||
return err
|
||||
// update the cluster set binding
|
||||
clusterSetName = fmt.Sprintf("clusterset-%s", rand.String(6))
|
||||
managedClusterSetBinding.Spec.ClusterSet = clusterSetName
|
||||
_, err = clusterClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Update(context.TODO(), managedClusterSetBinding, metav1.UpdateOptions{})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsBadRequest(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: The ManagedClusterSetBinding must have the same name as the target ManagedClusterSet",
|
||||
admissionName,
|
||||
)))
|
||||
})
|
||||
|
||||
ginkgo.It("should accept the request when updating the label of the ManagedClusterSetBinding by user without binding permission", func() {
|
||||
// create a cluster set binding
|
||||
clusterSetName := fmt.Sprintf("clusterset-%s", rand.String(6))
|
||||
managedClusterSetBinding := newManagedClusterSetBinding(namespace, clusterSetName, clusterSetName)
|
||||
managedClusterSetBinding, err := clusterClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Create(context.TODO(), managedClusterSetBinding, metav1.CreateOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
|
||||
// create a client without clusterset binding permission
|
||||
sa := fmt.Sprintf("webhook-sa-%s", rand.String(6))
|
||||
unauthorizedClient, err := buildClusterClient(namespace, sa, nil, []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclustersetbindings"},
|
||||
Verbs: []string{"create", "get", "update"},
|
||||
},
|
||||
})
|
||||
gomega.Expect(err).ToNot(gomega.HaveOccurred())
|
||||
|
||||
// update the cluster set binding by authorized user
|
||||
managedClusterSetBinding.Labels = map[string]string{
|
||||
"owner": "user1",
|
||||
}
|
||||
_, err = unauthorizedClient.ClusterV1alpha1().ManagedClusterSetBindings(namespace).Update(context.TODO(), managedClusterSetBinding, metav1.UpdateOptions{})
|
||||
gomega.Expect(err).NotTo(gomega.HaveOccurred())
|
||||
})
|
||||
gomega.Expect(err).To(gomega.HaveOccurred())
|
||||
gomega.Expect(errors.IsForbidden(err)).Should(gomega.BeTrue())
|
||||
gomega.Expect(err.Error()).Should(gomega.Equal(fmt.Sprintf(
|
||||
"admission webhook \"%s\" denied the request: user \"system:serviceaccount:%s:%s\" cannot update the HubAcceptsClient field",
|
||||
admissionName,
|
||||
saNamespace,
|
||||
sa,
|
||||
)))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -204,7 +373,19 @@ func newManagedCluster(name string, accepted bool, externalURL string) *clusterv
|
||||
}
|
||||
}
|
||||
|
||||
func buildUnauthorizedClusterClient(saName string) (clusterv1client.Interface, error) {
|
||||
func newManagedClusterSetBinding(namespace, name string, clusterSet string) *clusterv1alpha1.ManagedClusterSetBinding {
|
||||
return &clusterv1alpha1.ManagedClusterSetBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
},
|
||||
Spec: clusterv1alpha1.ManagedClusterSetBindingSpec{
|
||||
ClusterSet: clusterSet,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildClusterClient(saNamespace, saName string, clusterPolicyRules, policyRules []rbacv1.PolicyRule) (clusterv1client.Interface, error) {
|
||||
var err error
|
||||
|
||||
sa := &corev1.ServiceAccount{
|
||||
@@ -218,44 +399,86 @@ func buildUnauthorizedClusterClient(saName string) (clusterv1client.Interface, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clusterRoleName := fmt.Sprintf("%s-clusterrole", saName)
|
||||
clusterRole := &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclusters"},
|
||||
Verbs: []string{"create", "get", "update"},
|
||||
// create cluster role/rolebinding
|
||||
if len(clusterPolicyRules) > 0 {
|
||||
clusterRoleName := fmt.Sprintf("%s-clusterrole", saName)
|
||||
clusterRole := &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = hubClient.RbacV1().ClusterRoles().Create(context.TODO(), clusterRole, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
Rules: clusterPolicyRules,
|
||||
}
|
||||
_, err = hubClient.RbacV1().ClusterRoles().Create(context.TODO(), clusterRole, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-clusterrolebinding", saName),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: saNamespace,
|
||||
Name: saName,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
}
|
||||
_, err = hubClient.RbacV1().ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
clusterRoleBinding := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-clusterrolebinding", saName),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
// create cluster role/rolebinding
|
||||
if len(policyRules) > 0 {
|
||||
roleName := fmt.Sprintf("%s-role", saName)
|
||||
role := &rbacv1.Role{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: saNamespace,
|
||||
Name: saName,
|
||||
Name: roleName,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
}
|
||||
_, err = hubClient.RbacV1().ClusterRoleBindings().Create(context.TODO(), clusterRoleBinding, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"cluster.open-cluster-management.io"},
|
||||
Resources: []string{"managedclustersetbindings"},
|
||||
Verbs: []string{"create", "get", "update"},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = hubClient.RbacV1().Roles(saNamespace).Create(context.TODO(), role, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roleBinding := &rbacv1.RoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: saNamespace,
|
||||
Name: fmt.Sprintf("%s-rolebinding", saName),
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Namespace: saNamespace,
|
||||
Name: saName,
|
||||
},
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "Role",
|
||||
Name: roleName,
|
||||
},
|
||||
}
|
||||
_, err = hubClient.RbacV1().RoleBindings(saNamespace).Create(context.TODO(), roleBinding, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var tokenSecret *corev1.Secret
|
||||
|
||||
Reference in New Issue
Block a user