Skip to content

Commit fd8bc62

Browse files
committed
feat: webhooks
Signed-off-by: Artur Shad Nik <arturshadnik@gmail.com>
1 parent 1f6b81e commit fd8bc62

File tree

4 files changed

+522
-39
lines changed

4 files changed

+522
-39
lines changed

fleetconfig-controller/api/v1beta1/groupversion_info.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,18 @@ import (
2424
"sigs.k8s.io/controller-runtime/pkg/scheme"
2525
)
2626

27+
const group = "fleetconfig.open-cluster-management.io"
28+
2729
var (
30+
31+
// HubGroupKind is the group kind for the Hub API
32+
HubGroupKind = schema.GroupKind{Group: group, Kind: "Hub"}
33+
34+
// SpokeGroupKind is the group kind for the Spoke API
35+
SpokeGroupKind = schema.GroupKind{Group: group, Kind: "Spoke"}
36+
2837
// GroupVersion is group version used to register these objects.
29-
GroupVersion = schema.GroupVersion{Group: "fleetconfig.open-cluster-management.io", Version: "v1beta1"}
38+
GroupVersion = schema.GroupVersion{Group: group, Version: "v1beta1"}
3039

3140
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
3241
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

fleetconfig-controller/internal/webhook/v1beta1/hub_webhook.go

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// TODO - remove once hub webhooks are implemented.
18-
//
19-
//nolint:all // Required because of `dupl` between this file and spoke_webhook.go
2017
package v1beta1
2118

2219
import (
2320
"context"
2421
"fmt"
2522

23+
"k8s.io/apimachinery/pkg/api/errors"
2624
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/util/validation/field"
2726
ctrl "sigs.k8s.io/controller-runtime"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
2828
logf "sigs.k8s.io/controller-runtime/pkg/log"
2929
"sigs.k8s.io/controller-runtime/pkg/webhook"
3030
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3131

32-
fleetconfigopenclustermanagementiov1beta1 "github.com/open-cluster-management-io/lab/fleetconfig-controller/api/v1beta1"
32+
"github.com/open-cluster-management-io/lab/fleetconfig-controller/api/v1beta1"
3333
)
3434

3535
// nolint:unused
@@ -38,14 +38,12 @@ var hublog = logf.Log.WithName("hub-resource")
3838

3939
// SetupHubWebhookWithManager registers the webhook for Hub in the manager.
4040
func SetupHubWebhookWithManager(mgr ctrl.Manager) error {
41-
return ctrl.NewWebhookManagedBy(mgr).For(&fleetconfigopenclustermanagementiov1beta1.Hub{}).
42-
WithValidator(&HubCustomValidator{}).
41+
return ctrl.NewWebhookManagedBy(mgr).For(&v1beta1.Hub{}).
42+
WithValidator(&HubCustomValidator{client: mgr.GetClient()}).
4343
WithDefaulter(&HubCustomDefaulter{}).
4444
Complete()
4545
}
4646

47-
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
48-
4947
// +kubebuilder:webhook:path=/mutate-fleetconfig-open-cluster-management-io-v1beta1-hub,mutating=true,failurePolicy=fail,sideEffects=None,groups=fleetconfig.open-cluster-management.io,resources=hubs,verbs=create;update,versions=v1beta1,name=mhub-v1beta1.kb.io,admissionReviewVersions=v1
5048

5149
// HubCustomDefaulter struct is responsible for setting default values on the custom resource of the
@@ -61,7 +59,7 @@ var _ webhook.CustomDefaulter = &HubCustomDefaulter{}
6159

6260
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Hub.
6361
func (d *HubCustomDefaulter) Default(_ context.Context, obj runtime.Object) error {
64-
hub, ok := obj.(*fleetconfigopenclustermanagementiov1beta1.Hub)
62+
hub, ok := obj.(*v1beta1.Hub)
6563

6664
if !ok {
6765
return fmt.Errorf("expected an Hub object but got %T", obj)
@@ -84,40 +82,68 @@ func (d *HubCustomDefaulter) Default(_ context.Context, obj runtime.Object) erro
8482
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
8583
// as this struct is used only for temporary operations and does not need to be deeply copied.
8684
type HubCustomValidator struct {
87-
// TODO(user): Add more fields as needed for validation
85+
client client.Client
8886
}
8987

9088
var _ webhook.CustomValidator = &HubCustomValidator{}
9189

9290
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Hub.
93-
func (v *HubCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
94-
hub, ok := obj.(*fleetconfigopenclustermanagementiov1beta1.Hub)
91+
func (v *HubCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
92+
hub, ok := obj.(*v1beta1.Hub)
9593
if !ok {
9694
return nil, fmt.Errorf("expected a Hub object but got %T", obj)
9795
}
9896
hublog.Info("Validation for Hub upon creation", "name", hub.GetName())
9997

100-
// TODO(user): fill in your validation logic upon object creation.
98+
var allErrs field.ErrorList
99+
100+
if valid, msg := isKubeconfigValid(hub.Spec.Kubeconfig); !valid {
101+
allErrs = append(allErrs, field.Invalid(
102+
field.NewPath("hub"), hub.Spec.Kubeconfig, msg),
103+
)
104+
}
105+
if hub.Spec.ClusterManager == nil && hub.Spec.SingletonControlPlane == nil {
106+
allErrs = append(allErrs, field.Invalid(
107+
field.NewPath("hub"), hub.Spec, "either hub.clusterManager or hub.singletonControlPlane must be specified"),
108+
)
109+
}
110+
111+
allErrs = append(allErrs, validateHubAddons(ctx, v.client, nil, hub)...)
101112

113+
if len(allErrs) > 0 {
114+
return nil, errors.NewInvalid(v1beta1.HubGroupKind, hub.Name, allErrs)
115+
}
102116
return nil, nil
103117
}
104118

105119
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Hub.
106-
func (v *HubCustomValidator) ValidateUpdate(_ context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
107-
hub, ok := newObj.(*fleetconfigopenclustermanagementiov1beta1.Hub)
120+
func (v *HubCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
121+
hub, ok := newObj.(*v1beta1.Hub)
108122
if !ok {
109123
return nil, fmt.Errorf("expected a Hub object for the newObj but got %T", newObj)
110124
}
125+
oldHub, ok := oldObj.(*v1beta1.Hub)
126+
if !ok {
127+
return nil, fmt.Errorf("expected a Hub object for the oldObj but got %T", newObj)
128+
}
111129
hublog.Info("Validation for Hub upon update", "name", hub.GetName())
112130

113-
// TODO(user): fill in your validation logic upon object update.
131+
err := allowHubUpdate(oldHub, hub)
132+
if err != nil {
133+
return nil, err
134+
}
135+
136+
allErrs := validateHubAddons(ctx, v.client, nil, hub)
114137

138+
if len(allErrs) > 0 {
139+
return nil, errors.NewInvalid(v1beta1.HubGroupKind, hub.Name, allErrs)
140+
}
115141
return nil, nil
116142
}
117143

118144
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Hub.
119145
func (v *HubCustomValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
120-
hub, ok := obj.(*fleetconfigopenclustermanagementiov1beta1.Hub)
146+
hub, ok := obj.(*v1beta1.Hub)
121147
if !ok {
122148
return nil, fmt.Errorf("expected a Hub object but got %T", obj)
123149
}

fleetconfig-controller/internal/webhook/v1beta1/spoke_webhook.go

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,23 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
// TODO - remove once spoke webhooks are implemented.
18-
//
19-
//nolint:all // Required because of `dupl` between this file and spoke_webhook.go
2017
package v1beta1
2118

2219
import (
2320
"context"
2421
"fmt"
2522

23+
"k8s.io/apimachinery/pkg/api/errors"
2624
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/util/validation/field"
26+
operatorv1 "open-cluster-management.io/api/operator/v1"
2727
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
2829
logf "sigs.k8s.io/controller-runtime/pkg/log"
2930
"sigs.k8s.io/controller-runtime/pkg/webhook"
3031
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3132

32-
fleetconfigopenclustermanagementiov1beta1 "github.com/open-cluster-management-io/lab/fleetconfig-controller/api/v1beta1"
33+
"github.com/open-cluster-management-io/lab/fleetconfig-controller/api/v1beta1"
3334
)
3435

3536
// nolint:unused
@@ -38,14 +39,12 @@ var spokelog = logf.Log.WithName("spoke-resource")
3839

3940
// SetupSpokeWebhookWithManager registers the webhook for Spoke in the manager.
4041
func SetupSpokeWebhookWithManager(mgr ctrl.Manager) error {
41-
return ctrl.NewWebhookManagedBy(mgr).For(&fleetconfigopenclustermanagementiov1beta1.Spoke{}).
42-
WithValidator(&SpokeCustomValidator{}).
42+
return ctrl.NewWebhookManagedBy(mgr).For(&v1beta1.Spoke{}).
43+
WithValidator(&SpokeCustomValidator{client: mgr.GetClient()}).
4344
WithDefaulter(&SpokeCustomDefaulter{}).
4445
Complete()
4546
}
4647

47-
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
48-
4948
// +kubebuilder:webhook:path=/mutate-fleetconfig-open-cluster-management-io-v1beta1-spoke,mutating=true,failurePolicy=fail,sideEffects=None,groups=fleetconfig.open-cluster-management.io,resources=spokes,verbs=create;update,versions=v1beta1,name=mspoke-v1beta1.kb.io,admissionReviewVersions=v1
5049

5150
// SpokeCustomDefaulter struct is responsible for setting default values on the custom resource of the
@@ -61,15 +60,13 @@ var _ webhook.CustomDefaulter = &SpokeCustomDefaulter{}
6160

6261
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Spoke.
6362
func (d *SpokeCustomDefaulter) Default(_ context.Context, obj runtime.Object) error {
64-
spoke, ok := obj.(*fleetconfigopenclustermanagementiov1beta1.Spoke)
63+
spoke, ok := obj.(*v1beta1.Spoke)
6564

6665
if !ok {
6766
return fmt.Errorf("expected an Spoke object but got %T", obj)
6867
}
6968
spokelog.Info("Defaulting for Spoke", "name", spoke.GetName())
7069

71-
// TODO(user): fill in your defaulting logic.
72-
7370
return nil
7471
}
7572

@@ -84,40 +81,79 @@ func (d *SpokeCustomDefaulter) Default(_ context.Context, obj runtime.Object) er
8481
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
8582
// as this struct is used only for temporary operations and does not need to be deeply copied.
8683
type SpokeCustomValidator struct {
87-
// TODO(user): Add more fields as needed for validation
84+
client client.Client
8885
}
8986

9087
var _ webhook.CustomValidator = &SpokeCustomValidator{}
9188

9289
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Spoke.
93-
func (v *SpokeCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
94-
spoke, ok := obj.(*fleetconfigopenclustermanagementiov1beta1.Spoke)
90+
func (v *SpokeCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
91+
spoke, ok := obj.(*v1beta1.Spoke)
9592
if !ok {
9693
return nil, fmt.Errorf("expected a Spoke object but got %T", obj)
9794
}
9895
spokelog.Info("Validation for Spoke upon creation", "name", spoke.GetName())
9996

100-
// TODO(user): fill in your validation logic upon object creation.
97+
var allErrs field.ErrorList
98+
99+
if spoke.Spec.Klusterlet.Mode == string(operatorv1.InstallModeHosted) {
100+
if spoke.Spec.Klusterlet.ManagedClusterKubeconfig.SecretReference == nil {
101+
allErrs = append(allErrs, field.Invalid(
102+
field.NewPath("spec").Child("klusterlet").Child("managedClusterKubeconfig").Child("secretReference"),
103+
spoke.Name, "managedClusterKubeconfig.secretReference is required in hosted mode"),
104+
)
105+
} else {
106+
if valid, msg := isKubeconfigValid(spoke.Spec.Klusterlet.ManagedClusterKubeconfig); !valid {
107+
allErrs = append(allErrs, field.Invalid(
108+
field.NewPath("spec").Child("klusterlet").Child("managedClusterKubeconfig").Child("secretReference"),
109+
spoke.Name, msg),
110+
)
111+
}
112+
}
113+
}
114+
if valid, msg := isKubeconfigValid(spoke.Spec.Kubeconfig); !valid {
115+
allErrs = append(allErrs, field.Invalid(
116+
field.NewPath("spec").Child("kubeconfig"), spoke, msg),
117+
)
118+
}
119+
120+
warn, errs := validateAddons(ctx, v.client, spoke)
121+
allErrs = append(allErrs, errs...)
101122

102-
return nil, nil
123+
if len(allErrs) > 0 {
124+
return warn, errors.NewInvalid(v1beta1.HubGroupKind, spoke.Name, allErrs)
125+
}
126+
return warn, nil
103127
}
104128

105129
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Spoke.
106-
func (v *SpokeCustomValidator) ValidateUpdate(_ context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
107-
spoke, ok := newObj.(*fleetconfigopenclustermanagementiov1beta1.Spoke)
130+
func (v *SpokeCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
131+
spoke, ok := newObj.(*v1beta1.Spoke)
132+
if !ok {
133+
return nil, fmt.Errorf("expected a Spoke object for the newObj but got %T", newObj)
134+
}
135+
oldSpoke, ok := oldObj.(*v1beta1.Spoke)
108136
if !ok {
109137
return nil, fmt.Errorf("expected a Spoke object for the newObj but got %T", newObj)
110138
}
111139
spokelog.Info("Validation for Spoke upon update", "name", spoke.GetName())
112140

113-
// TODO(user): fill in your validation logic upon object update.
141+
err := allowSpokeUpdate(oldSpoke, spoke)
142+
if err != nil {
143+
return nil, err
144+
}
114145

115-
return nil, nil
146+
warn, allErrs := validateAddons(ctx, v.client, spoke)
147+
148+
if len(allErrs) > 0 {
149+
return warn, errors.NewInvalid(v1beta1.SpokeGroupKind, spoke.Name, allErrs)
150+
}
151+
return warn, nil
116152
}
117153

118154
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Spoke.
119155
func (v *SpokeCustomValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
120-
spoke, ok := obj.(*fleetconfigopenclustermanagementiov1beta1.Spoke)
156+
spoke, ok := obj.(*v1beta1.Spoke)
121157
if !ok {
122158
return nil, fmt.Errorf("expected a Spoke object but got %T", obj)
123159
}

0 commit comments

Comments
 (0)