diff --git a/pkg/extensionapis/walrus/schema.go b/pkg/extensionapis/walrus/schema.go index 1732312c0..ac06b99b2 100644 --- a/pkg/extensionapis/walrus/schema.go +++ b/pkg/extensionapis/walrus/schema.go @@ -2,7 +2,6 @@ package walrus import ( "context" - "strings" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -12,10 +11,7 @@ import ( walrus "github.com/seal-io/walrus/pkg/apis/walrus/v1" walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" - "github.com/seal-io/walrus/pkg/apistatus" "github.com/seal-io/walrus/pkg/extensionapi" - "github.com/seal-io/walrus/pkg/systemmeta" - "github.com/seal-io/walrus/pkg/templates/kubehelper" ) // SchemaHandler handles v1.Schema objects. @@ -96,31 +92,3 @@ func (h *SchemaHandler) CastObjectTo(do *walrus.Schema) (uo *walruscore.Schema) func (h *SchemaHandler) CastObjectFrom(uo *walruscore.Schema) (do *walrus.Schema) { return (*walrus.Schema)(uo) } - -func (h *SchemaHandler) OnUpdate(ctx context.Context, obj, _ runtime.Object, opts ctrlcli.UpdateOptions) (runtime.Object, error) { - s := obj.(*walrus.Schema) - - if apistatus.SchemaStatusReset.IsTrue(obj) { - var ( - ouis walruscore.Schema - ouisKey = ctrlcli.ObjectKey{ - Namespace: s.Namespace, - Name: strings.TrimSuffix(s.Name, walruscore.NameSuffixUISchema) + walruscore.NameSuffixOriginalUISchema, - } - ) - err := h.client.Get(ctx, ouisKey, &ouis) - if err != nil { - return nil, err - } - - apistatus.SchemaStatusReset.False(s, "", "") - systemmeta.UnnoteResource(s) - s.Status.Value = ouis.Status.Value - } else { - systemmeta.NoteResource(s, "", map[string]string{kubehelper.SchemaUserEditedNote: "true"}) - } - - uo := h.CastObjectTo(s) - err := h.client.Update(ctx, uo, &opts) - return h.CastObjectFrom(uo), err -} diff --git a/pkg/templates/kubehelper/schema.go b/pkg/templates/kubehelper/schema.go index 05913493e..4f71c37b1 100644 --- a/pkg/templates/kubehelper/schema.go +++ b/pkg/templates/kubehelper/schema.go @@ -86,7 +86,12 @@ func GenTemplateVersion( func createOrUpdateSchema(ctx context.Context, loopbackKubeCli clientset.Interface, t *walruscore.Template, name string, data []byte) error { cli := loopbackKubeCli.WalruscoreV1().Schemas(t.Namespace) - var es *walruscore.Schema + var ( + es *walruscore.Schema + editBySystemAnn = map[string]string{ + SchemaUserEditedNote: "false", + } + ) { es = &walruscore.Schema{ ObjectMeta: meta.ObjectMeta{ @@ -101,9 +106,14 @@ func createOrUpdateSchema(ctx context.Context, loopbackKubeCli clientset.Interfa }, } kubemeta.ControlOn(es, t, walruscore.SchemeGroupVersion.WithKind("Template")) + systemmeta.NoteResource(es, "", editBySystemAnn) } alignFn := func(as *walruscore.Schema) (*walruscore.Schema, bool, error) { + if systemmeta.DescribeResourceNote(as, SchemaUserEditedNote) != "true" { + systemmeta.NoteResource(as, "", editBySystemAnn) + } + if bytes.Equal(as.Status.Value.Raw, data) { return as, true, nil } diff --git a/pkg/webhooks/setup.go b/pkg/webhooks/setup.go index 9c5bcbf9a..8bf531579 100644 --- a/pkg/webhooks/setup.go +++ b/pkg/webhooks/setup.go @@ -27,6 +27,7 @@ var ( new(walruscore.ConnectorBindingWebhook), new(walruscore.ResourceDefinitionWebhook), new(walruscore.TemplateWebhook), + new(walruscore.SchemaWebhook), } cfgGetters = []_WebhookConfigurationsGetter{ walruscore.GetWebhookConfigurations, diff --git a/pkg/webhooks/walruscore/schema.go b/pkg/webhooks/walruscore/schema.go new file mode 100644 index 000000000..f8b126dfe --- /dev/null +++ b/pkg/webhooks/walruscore/schema.go @@ -0,0 +1,107 @@ +package walruscore + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrlcli "sigs.k8s.io/controller-runtime/pkg/client" + ctrlwebhook "sigs.k8s.io/controller-runtime/pkg/webhook" + ctrladmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + walruscore "github.com/seal-io/walrus/pkg/apis/walruscore/v1" + "github.com/seal-io/walrus/pkg/apistatus" + "github.com/seal-io/walrus/pkg/systemmeta" + "github.com/seal-io/walrus/pkg/templates/api" + "github.com/seal-io/walrus/pkg/templates/kubehelper" + "github.com/seal-io/walrus/pkg/webhook" +) + +// SchemaWebhook hooks a v1.Schema object. +// +// nolint: lll +// nolint: lll +// +k8s:webhook-gen:mutating:group="walruscore.seal.io",version="v1",resource="schemas",scope="Namespaced" +// +k8s:webhook-gen:mutating:operations=["UPDATE"],failurePolicy="Fail",sideEffects="None",matchPolicy="Equivalent",timeoutSeconds=10 +// +k8s:webhook-gen:validating:group="walruscore.seal.io",version="v1",resource="schemas",scope="Namespaced" +// +k8s:webhook-gen:validating:operations=["UPDATE"],failurePolicy="Fail",sideEffects="None",matchPolicy="Equivalent",timeoutSeconds=10 +type SchemaWebhook struct { + webhook.DefaultCustomValidator + + client ctrlcli.Client +} + +func (r *SchemaWebhook) SetupWebhook(_ context.Context, opts webhook.SetupOptions) (runtime.Object, error) { + r.client = opts.Manager.GetClient() + + return &walruscore.Schema{}, nil +} + +var ( + _ ctrlwebhook.CustomValidator = (*SchemaWebhook)(nil) + _ ctrlwebhook.CustomDefaulter = (*SchemaWebhook)(nil) +) + +func (r *SchemaWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (ctrladmission.Warnings, error) { + s := newObj.(*walruscore.Schema) + + if s.Status.Value.Size() == 0 { + return nil, field.Invalid( + field.NewPath("status.value"), "", "empty schema") + } + + var ws api.WrapSchema + err := json.Unmarshal(s.Status.Value.Raw, &ws) + if err != nil { + return nil, field.Invalid( + field.NewPath("status.value"), "", err.Error()) + } + + err = ws.Validate() + if err != nil { + return nil, field.Invalid( + field.NewPath("status.value"), "", err.Error()) + } + + return nil, nil +} + +func (r *SchemaWebhook) Default(ctx context.Context, obj runtime.Object) error { + s := obj.(*walruscore.Schema) + + // Skip template schema and original ui schema. + if strings.HasSuffix(s.Name, walruscore.NameSuffixTemplateSchema) || strings.HasSuffix(s.Name, walruscore.NameSuffixOriginalUISchema) { + return nil + } + + switch { + case systemmeta.DescribeResourceNote(s, kubehelper.SchemaUserEditedNote) == "false": + // System edit. + case apistatus.SchemaStatusReset.IsTrue(obj): + // User reset. + var ( + ouis walruscore.Schema + ouisKey = ctrlcli.ObjectKey{ + Namespace: s.Namespace, + Name: strings.TrimSuffix(s.Name, walruscore.NameSuffixUISchema) + walruscore.NameSuffixOriginalUISchema, + } + ) + err := r.client.Get(ctx, ouisKey, &ouis) + if err != nil { + return fmt.Errorf("failed to find %s: %w", ouisKey.String(), err) + } + + apistatus.SchemaStatusReset.False(s, "", "") + systemmeta.UnnoteResource(s) + s.Status.Value = ouis.Status.Value + + default: + // User edit. + systemmeta.NoteResource(s, "", map[string]string{kubehelper.SchemaUserEditedNote: "true"}) + } + + return nil +} diff --git a/pkg/webhooks/walruscore/zz_generated.webhooks.go b/pkg/webhooks/walruscore/zz_generated.webhooks.go index 2f626cff9..a7537cb5b 100644 --- a/pkg/webhooks/walruscore/zz_generated.webhooks.go +++ b/pkg/webhooks/walruscore/zz_generated.webhooks.go @@ -33,6 +33,7 @@ func GetValidatingWebhookConfiguration(n string, c v1.WebhookClientConfig) *v1.V vwh_walrus_pkg_webhooks_walruscore_CatalogWebhook(c), vwh_walrus_pkg_webhooks_walruscore_ConnectorBindingWebhook(c), vwh_walrus_pkg_webhooks_walruscore_ConnectorWebhook(c), + vwh_walrus_pkg_webhooks_walruscore_SchemaWebhook(c), vwh_walrus_pkg_webhooks_walruscore_TemplateWebhook(c), }, } @@ -51,6 +52,7 @@ func GetMutatingWebhookConfiguration(n string, c v1.WebhookClientConfig) *v1.Mut mwh_walrus_pkg_webhooks_walruscore_ConnectorBindingWebhook(c), mwh_walrus_pkg_webhooks_walruscore_ConnectorWebhook(c), mwh_walrus_pkg_webhooks_walruscore_ResourceDefinitionWebhook(c), + mwh_walrus_pkg_webhooks_walruscore_SchemaWebhook(c), }, } } @@ -340,6 +342,98 @@ func mwh_walrus_pkg_webhooks_walruscore_ResourceDefinitionWebhook(c v1.WebhookCl } } +func (*SchemaWebhook) ValidatePath() string { + return "/validate-walruscore-seal-io-v1-schema" +} + +func vwh_walrus_pkg_webhooks_walruscore_SchemaWebhook(c v1.WebhookClientConfig) v1.ValidatingWebhook { + path := "/validate-walruscore-seal-io-v1-schema" + + cc := c.DeepCopy() + if cc.Service != nil { + cc.Service.Path = &path + } else if c.URL != nil { + cc.URL = ptr.To(*c.URL + path) + } + + return v1.ValidatingWebhook{ + Name: "validate.walruscore.seal.io.v1.schema", + ClientConfig: *cc, + Rules: []v1.RuleWithOperations{ + { + Rule: v1.Rule{ + APIGroups: []string{ + "walruscore.seal.io", + }, + APIVersions: []string{ + "v1", + }, + Resources: []string{ + "schemas", + }, + Scope: ptr.To[v1.ScopeType]("Namespaced"), + }, + Operations: []v1.OperationType{ + "UPDATE", + }, + }, + }, + FailurePolicy: ptr.To[v1.FailurePolicyType]("Fail"), + MatchPolicy: ptr.To[v1.MatchPolicyType]("Equivalent"), + SideEffects: ptr.To[v1.SideEffectClass]("None"), + TimeoutSeconds: ptr.To[int32](10), + AdmissionReviewVersions: []string{ + "v1", + }, + } +} + +func (*SchemaWebhook) DefaultPath() string { + return "/mutate-walruscore-seal-io-v1-schema" +} + +func mwh_walrus_pkg_webhooks_walruscore_SchemaWebhook(c v1.WebhookClientConfig) v1.MutatingWebhook { + path := "/mutate-walruscore-seal-io-v1-schema" + + cc := c.DeepCopy() + if cc.Service != nil { + cc.Service.Path = &path + } else if c.URL != nil { + cc.URL = ptr.To(*c.URL + path) + } + + return v1.MutatingWebhook{ + Name: "mutate.walruscore.seal.io.v1.schema", + ClientConfig: *cc, + Rules: []v1.RuleWithOperations{ + { + Rule: v1.Rule{ + APIGroups: []string{ + "walruscore.seal.io", + }, + APIVersions: []string{ + "v1", + }, + Resources: []string{ + "schemas", + }, + Scope: ptr.To[v1.ScopeType]("Namespaced"), + }, + Operations: []v1.OperationType{ + "UPDATE", + }, + }, + }, + FailurePolicy: ptr.To[v1.FailurePolicyType]("Fail"), + MatchPolicy: ptr.To[v1.MatchPolicyType]("Equivalent"), + SideEffects: ptr.To[v1.SideEffectClass]("None"), + TimeoutSeconds: ptr.To[int32](10), + AdmissionReviewVersions: []string{ + "v1", + }, + } +} + func (*TemplateWebhook) ValidatePath() string { return "/validate-walruscore-seal-io-v1-template" }