Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validator for clusterproxyconfigs to make sure only one is ever created for a given cluster #327

Merged
merged 7 commits into from
Apr 3, 2024
Merged
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
9 changes: 9 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ If yes, the webhook redacts the role, so that it only grants a deletion permissi

# management.cattle.io/v3

## ClusterProxyConfig

### Validation Checks

#### On create

When creating a clusterproxyconfig, we check to make sure that one does not already exist for the given cluster.
Only 1 clusterproxyconfig per downstream cluster is ever permitted.

## ClusterRoleTemplateBinding

### Validation Checks
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ require (
github.com/gorilla/mux v1.8.0
github.com/rancher/dynamiclistener v0.5.0-rc2
github.com/rancher/lasso v0.0.0-20240123150939-7055397d6dfa
github.com/rancher/rancher/pkg/apis v0.0.0-20240318165156-941d6d3102a3
github.com/rancher/rancher/pkg/apis v0.0.0-20240328110445-91a4620d7e49
github.com/rancher/rke v1.5.7-rc2
github.com/rancher/wrangler/v2 v2.1.3
github.com/rancher/wrangler/v2 v2.1.4
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -301,14 +301,14 @@ github.com/rancher/lasso v0.0.0-20240123150939-7055397d6dfa h1:eRhvQJjIpPxJunlS3
github.com/rancher/lasso v0.0.0-20240123150939-7055397d6dfa/go.mod h1:utdskbIL7kdVvPCUFPEJQDWJwPHGFpUCRfVkX2G2Xxg=
github.com/rancher/norman v0.0.0-20240207153100-3bb70b772b52 h1:k9QmFfME7e92jP7X5iTtPHoOrM9Z/zXZQpAh5jzk6dE=
github.com/rancher/norman v0.0.0-20240207153100-3bb70b772b52/go.mod h1:WbNpu/HwChwKk54W0rWBdioxYVVZwVVz//UX84m/NvY=
github.com/rancher/rancher/pkg/apis v0.0.0-20240318165156-941d6d3102a3 h1:EiDpia9/RFbCVeR0FDRlZrapLMus6FzfU8D4NGg0k0M=
github.com/rancher/rancher/pkg/apis v0.0.0-20240318165156-941d6d3102a3/go.mod h1:L88OxsOljRR0bv1dskAY7D04JauYxxw2qaCCHMir2IY=
github.com/rancher/rancher/pkg/apis v0.0.0-20240328110445-91a4620d7e49 h1:rjjp6WJiodb3sSQtgd9VOKNnzT3dlObuE2lkftFoF64=
github.com/rancher/rancher/pkg/apis v0.0.0-20240328110445-91a4620d7e49/go.mod h1:+uY2NLGZh5TR6x8IYPo5hcmnq6bl8XV2x+7jcNLAC5M=
github.com/rancher/rke v1.5.7-rc2 h1:DikVnA+6HU7m/UvmIsi5a8H4JWT5/ec1JXSQa9wvAnk=
github.com/rancher/rke v1.5.7-rc2/go.mod h1:+lcRKCxBLtfaSZQ9Q+BA82cHhSImF62mfElqJvHJUls=
github.com/rancher/wrangler v1.1.1 h1:wmqUwqc2M7ADfXnBCJTFkTB5ZREWpD78rnZMzmxwMvM=
github.com/rancher/wrangler v1.1.1/go.mod h1:ioVbKupzcBOdzsl55MvEDN0R1wdGggj8iNCYGFI5JvM=
github.com/rancher/wrangler/v2 v2.1.3 h1:ggCPFD14emodJjR4Pi6mcDGgtNo04tjCKZ71S76uWg8=
github.com/rancher/wrangler/v2 v2.1.3/go.mod h1:af5OaGU/COgreQh1mRbKiUI64draT2NN34uk+PALFY8=
github.com/rancher/wrangler/v2 v2.1.4 h1:ohov0i6A9dJHHO6sjfsH4Dqv93ZTdm5lIJVJdPzVdQc=
github.com/rancher/wrangler/v2 v2.1.4/go.mod h1:af5OaGU/COgreQh1mRbKiUI64draT2NN34uk+PALFY8=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
Expand Down
1 change: 1 addition & 0 deletions pkg/codegen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func main() {
v3.ProjectRoleTemplateBinding{},
v3.Node{},
v3.Project{},
v3.ClusterProxyConfig{},
},
},
"provisioning.cattle.io": {
Expand Down

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

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Validation Checks

### On create

When creating a clusterproxyconfig, we check to make sure that one does not already exist for the given cluster.
Only 1 clusterproxyconfig per downstream cluster is ever permitted.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package clusterproxyconfig

import (
"fmt"
"net/http"

"github.com/rancher/webhook/pkg/admission"
webhookadmission "github.com/rancher/webhook/pkg/admission"
controllerv3 "github.com/rancher/webhook/pkg/generated/controllers/management.cattle.io/v3"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/trace"
)

var gvr = schema.GroupVersionResource{
Group: "management.cattle.io",
Version: "v3",
Resource: "clusterproxyconfigs",
}

// Validator for validating clusterproxyconfigs.
type Validator struct {
admitter admitter
}

// NewValidator returns a new validator for clusterproxyconfigs.
func NewValidator(cpsCache controllerv3.ClusterProxyConfigCache) *Validator {
return &Validator{
admitter: admitter{
cpsCache: cpsCache,
},
}
}

// GVR returns the GroupVersionKind for this CRD.
func (v *Validator) GVR() schema.GroupVersionResource {
return gvr
}

// Operations returns list of operations handled by this validator.
func (v *Validator) Operations() []admissionregistrationv1.OperationType {
return []admissionregistrationv1.OperationType{admissionregistrationv1.Create}
}

// ValidatingWebhook returns the ValidatingWebhook used for this CRD.
func (v *Validator) ValidatingWebhook(clientConfig admissionregistrationv1.WebhookClientConfig) []admissionregistrationv1.ValidatingWebhook {
valWebhook := admission.NewDefaultValidatingWebhook(v, clientConfig, admissionregistrationv1.NamespacedScope, v.Operations())
valWebhook.FailurePolicy = admission.Ptr(admissionregistrationv1.Fail)
return []admissionregistrationv1.ValidatingWebhook{*valWebhook}
}

// Admitters returns the admitter objects used to validate clusterproxyconfigs.
func (v *Validator) Admitters() []admission.Admitter {
return []admission.Admitter{&v.admitter}
}

type admitter struct {
cpsCache controllerv3.ClusterProxyConfigCache
}

// Admit handles the webhook admission request sent to this webhook.
func (a *admitter) Admit(request *admission.Request) (*admissionv1.AdmissionResponse, error) {
listTrace := trace.New("clusterProxyConfigValidator Admit", trace.Field{Key: "user", Value: request.UserInfo.Username})
defer listTrace.LogIfLong(admission.SlowTraceDuration)

cps, err := a.cpsCache.List(request.Namespace, labels.Everything())
if err != nil {
return nil, fmt.Errorf("failed to fetch list of existing clusterproxyconfigs for clusterID %s: %w", request.Namespace, err)
}
// There can be no more than 1 clusterproxyconfig created per downstream cluster
if len(cps) > 0 {
return &admissionv1.AdmissionResponse{
Result: &metav1.Status{
Status: "Failure",
Message: fmt.Sprintf("there may only be one clusterproxyconfig object defined per cluster"),
Reason: metav1.StatusReasonConflict,
Code: http.StatusConflict,
},
Allowed: false,
}, nil
}

return webhookadmission.ResponseAllowed(), nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package clusterproxyconfig

import (
"fmt"
"testing"

"github.com/golang/mock/gomock"
v3api "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
"github.com/rancher/webhook/pkg/admission"
wranglerfake "github.com/rancher/wrangler/v2/pkg/generic/fake"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
admissionv1 "k8s.io/api/admission/v1"
authenicationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
)

const (
testNamespace = "testclusternamespace"
)

var (
cpcGVR = metav1.GroupVersionResource{Group: "management.cattle.io", Version: "v3", Resource: "clusterproxyconfigs"}
cpcGVK = metav1.GroupVersionKind{Group: "management.cattle.io", Version: "v3", Kind: "ClusterProxyConfig"}
)

func Test_admitter_Admit(t *testing.T) {
crobby marked this conversation as resolved.
Show resolved Hide resolved
tests := []struct {
name string
alreadyExists bool
allowed bool
wantErr bool
}{
{
name: "create clusterproxyconfig when none exists",
allowed: true,
alreadyExists: false,
},
{
name: "attempt to make more than one clusterproxyconfig",
allowed: false,
alreadyExists: true,
},
{
name: "failed to list clusterproxyconfigs",
wantErr: true,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
cpsCache := wranglerfake.NewMockCacheInterface[*v3api.ClusterProxyConfig](ctrl)
cpsCache.EXPECT().List(testNamespace, labels.Everything()).DoAndReturn(func(_ string, _ labels.Selector) ([]*v3api.ClusterProxyConfig, error) {
if tt.wantErr {
return nil, fmt.Errorf("simulated list error")
}
if tt.alreadyExists {
return []*v3api.ClusterProxyConfig{
{
Enabled: true,
},
}, nil
}
return nil, nil
}).AnyTimes()
a := &admitter{
cpsCache: cpsCache,
}
resp, err := a.Admit(createRequest())
if !tt.wantErr {
require.NoError(t, err, "Admit returned an error")
assert.Equal(t, tt.allowed, resp.Allowed)
} else {
require.Error(t, err)
assert.Nil(t, resp)
}
})
}
}

func createRequest() *admission.Request {
req := admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Kind: cpcGVK,
Resource: cpcGVR,
RequestKind: &cpcGVK,
RequestResource: &cpcGVR,
Namespace: testNamespace,
Operation: admissionv1.Create,
UserInfo: authenicationv1.UserInfo{Username: "test-user", UID: ""},
Object: runtime.RawExtension{},
OldObject: runtime.RawExtension{},
},
}
return &req
}
4 changes: 3 additions & 1 deletion pkg/server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
nshandler "github.com/rancher/webhook/pkg/resources/core/v1/namespace"
"github.com/rancher/webhook/pkg/resources/core/v1/secret"
managementCluster "github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/cluster"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/clusterproxyconfig"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/clusterroletemplatebinding"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/feature"
"github.com/rancher/webhook/pkg/resources/management.cattle.io/v3/fleetworkspace"
Expand Down Expand Up @@ -34,6 +35,7 @@ func Validation(clients *clients.Clients) ([]admission.ValidatingAdmissionHandle
}

if clients.MultiClusterManagement {
clusterProxyConfigs := clusterproxyconfig.NewValidator(clients.Management.ClusterProxyConfig().Cache())
crtbResolver := resolvers.NewCRTBRuleResolver(clients.Management.ClusterRoleTemplateBinding().Cache(), clients.RoleTemplateResolver)
prtbResolver := resolvers.NewPRTBRuleResolver(clients.Management.ProjectRoleTemplateBinding().Cache(), clients.RoleTemplateResolver)
grbResolver := resolvers.NewGRBClusterRuleResolver(clients.Management.GlobalRoleBinding().Cache(), clients.GlobalRoleResolver)
Expand All @@ -49,7 +51,7 @@ func Validation(clients *clients.Clients) ([]admission.ValidatingAdmissionHandle
roles := role.NewValidator()
rolebindings := rolebinding.NewValidator()

handlers = append(handlers, psact, globalRoles, globalRoleBindings, prtbs, crtbs, roleTemplates, secrets, nodeDriver, projects, roles, rolebindings)
handlers = append(handlers, psact, globalRoles, globalRoleBindings, prtbs, crtbs, roleTemplates, secrets, nodeDriver, projects, roles, rolebindings, clusterProxyConfigs)
}
return handlers, nil
}
Expand Down
45 changes: 45 additions & 0 deletions tests/integration/clusterProxyConfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package integration_test

import (
"context"
"time"

v3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// TestClusterProxyConfig tests that the webhook correctly limits the number of
// ClusterProxyConfig objects that can exist in a single namespace.
func (m *IntegrationSuite) TestClusterProxyConfig() {
objGVK := schema.GroupVersionKind{
Group: "management.cattle.io",
Version: "v3",
Kind: "ClusterProxyConfig",
}
validCreateObj := getObjectToCreate("testclusterproxyconfig")
client, err := m.clientFactory.ForKind(objGVK)
m.Require().NoError(err, "Failed to create client")
result := &v3.ClusterProxyConfig{}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
// Creating the first CPC should succeed
err = client.Create(ctx, validCreateObj.Namespace, validCreateObj, result, metav1.CreateOptions{})
m.NoError(err, "Error returned during the creation of a valid clusterProxyConfig")
secondCPC := getObjectToCreate("anotherclusterproxyconfig")
// Attempting to create another CPC in the same namespace should fail
err = client.Create(ctx, validCreateObj.Namespace, secondCPC, result, metav1.CreateOptions{})
m.Error(err, "Error was not returned when attempting to create a second clusterProxyConfig")
err = client.Delete(ctx, validCreateObj.Namespace, validCreateObj.Name, metav1.DeleteOptions{})
m.NoError(err, "Error returned during the deletion of a clusterProxyConfig")
}

func getObjectToCreate(name string) *v3.ClusterProxyConfig {
return &v3.ClusterProxyConfig{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: testNamespace,
},
Enabled: true,
}
}