From b6f544b24f73c611cd14eac3796c35553d9985ba Mon Sep 17 00:00:00 2001 From: Nicolas Bigler Date: Thu, 21 Nov 2024 14:31:33 +0100 Subject: [PATCH 1/5] Use an array instead of a string for the FQDN parameter We want to allow setting multiple FQDNs for the nextcloud service, so that the end user can specify multiple endpoints. Signed-off-by: Nicolas Bigler --- apis/vshn/v1/vshn_nextcloud.go | 5 +- apis/vshn/v1/zz_generated.deepcopy.go | 5 + crds/vshn.appcat.vshn.io_vshnnextclouds.yaml | 7 +- crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml | 7 +- .../functions/vshnnextcloud/deploy.go | 12 +- .../functions/vshnnextcloud/deploy_test.go | 11 ++ .../functions/vshnnextcloud/ingress.go | 9 +- test/functions/vshnnextcloud/01_default.yaml | 4 + test/functions/vshnnextcloud/01_no_fqdn.yaml | 183 ++++++++++++++++++ test/functions/vshnnextcloud/02_no_pg.yaml | 2 + test/functions/vshnnextcloud/02_withFQDN.yaml | 3 +- 11 files changed, 235 insertions(+), 13 deletions(-) create mode 100644 test/functions/vshnnextcloud/01_no_fqdn.yaml diff --git a/apis/vshn/v1/vshn_nextcloud.go b/apis/vshn/v1/vshn_nextcloud.go index 62511382c9..8c7ef4410f 100644 --- a/apis/vshn/v1/vshn_nextcloud.go +++ b/apis/vshn/v1/vshn_nextcloud.go @@ -91,11 +91,12 @@ type VSHNNextcloudServiceSpec struct { // Collabora contains settings to control the Collabora integration. Collabora CollaboraSpec `json:"collabora,omitempty"` // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 - // FQDN contains the FQDN which will be used for the ingress. + // FQDN contains the FQDNs which will be used for the ingress. // If it's not set, no ingress will be deployed. // This also enables strict hostname checking for this FQDN. - FQDN string `json:"fqdn"` + FQDN []string `json:"fqdn"` // RelativePath on which Nextcloud will listen. // +kubebuilder:default="/" diff --git a/apis/vshn/v1/zz_generated.deepcopy.go b/apis/vshn/v1/zz_generated.deepcopy.go index eb06466284..737733e91f 100644 --- a/apis/vshn/v1/zz_generated.deepcopy.go +++ b/apis/vshn/v1/zz_generated.deepcopy.go @@ -900,6 +900,11 @@ func (in *VSHNNextcloudParameters) DeepCopy() *VSHNNextcloudParameters { func (in *VSHNNextcloudServiceSpec) DeepCopyInto(out *VSHNNextcloudServiceSpec) { *out = *in out.Collabora = in.Collabora + if in.FQDN != nil { + in, out := &in.FQDN, &out.FQDN + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.PostgreSQLParameters != nil { in, out := &in.PostgreSQLParameters, &out.PostgreSQLParameters *out = new(VSHNPostgreSQLParameters) diff --git a/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml b/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml index 4fa8dc3a25..28a2a0843a 100644 --- a/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml +++ b/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml @@ -4866,10 +4866,13 @@ spec: type: object fqdn: description: |- - FQDN contains the FQDN which will be used for the ingress. + FQDN contains the FQDNs which will be used for the ingress. If it's not set, no ingress will be deployed. This also enables strict hostname checking for this FQDN. - type: string + items: + type: string + minItems: 1 + type: array postgreSQLParameters: description: |- PostgreSQLParameters can be used to set any supported setting in the diff --git a/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml b/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml index 7e1dd0a4c0..69ee177683 100644 --- a/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml @@ -5595,10 +5595,13 @@ spec: type: object fqdn: description: |- - FQDN contains the FQDN which will be used for the ingress. + FQDN contains the FQDNs which will be used for the ingress. If it's not set, no ingress will be deployed. This also enables strict hostname checking for this FQDN. - type: string + items: + type: string + minItems: 1 + type: array postgreSQLParameters: description: |- PostgreSQLParameters can be used to set any supported setting in the diff --git a/pkg/comp-functions/functions/vshnnextcloud/deploy.go b/pkg/comp-functions/functions/vshnnextcloud/deploy.go index bd0e5ab4ed..34fd51c347 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/deploy.go +++ b/pkg/comp-functions/functions/vshnnextcloud/deploy.go @@ -256,10 +256,20 @@ func newValues(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.VS } } + if len(comp.Spec.Parameters.Service.FQDN) == 0 { + return nil, fmt.Errorf("FQDN array is empty, but requires at least one entry: %w", errors.New("empty fqdn")) + } + + trustedDomain := []string{ + comp.GetName() + "." + comp.GetInstanceNamespace() + ".svc.cluster.local", + } + trustedDomain = append(trustedDomain, comp.Spec.Parameters.Service.FQDN...) + updatedNextcloudConfig := setBackgroundJobMaintenance(comp.Spec.Parameters.Maintenance.GetMaintenanceTimeOfDay(), nextcloudConfig) values = map[string]any{ "nextcloud": map[string]any{ - "host": comp.Spec.Parameters.Service.FQDN, + "host": comp.Spec.Parameters.Service.FQDN[0], + "trustedDomains": trustedDomain, "existingSecret": map[string]any{ "enabled": true, "secretName": adminSecret, diff --git a/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go b/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go index 7af08c5bdb..bdb234a8a5 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go +++ b/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go @@ -4,6 +4,8 @@ import ( "context" _ "embed" "encoding/json" + "errors" + "fmt" "strings" "testing" @@ -36,6 +38,15 @@ func Test_addNextcloud(t *testing.T) { } +func Test_addNextcloud_no_fqdn(t *testing.T) { + svc, _ := getNextcloudComp(t, "vshnnextcloud/01_no_fqdn.yaml") + + ctx := context.TODO() + + assert.Equal(t, DeployNextcloud(ctx, &vshnv1.VSHNNextcloud{}, svc), runtime.NewWarningResult(fmt.Sprintf("cannot create release: FQDN array is empty, but requires at least one entry: %s", errors.New("empty fqdn")))) + +} + func Test_addReleaseInternalDB(t *testing.T) { svc, comp := getNextcloudComp(t, "vshnnextcloud/02_no_pg.yaml") diff --git a/pkg/comp-functions/functions/vshnnextcloud/ingress.go b/pkg/comp-functions/functions/vshnnextcloud/ingress.go index 404e11bcb7..04bb36c052 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/ingress.go +++ b/pkg/comp-functions/functions/vshnnextcloud/ingress.go @@ -3,6 +3,7 @@ package vshnnextcloud import ( "context" "encoding/json" + "errors" "fmt" xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" @@ -22,8 +23,8 @@ func AddIngress(_ context.Context, comp *vshnv1.VSHNNextcloud, svc *runtime.Serv return runtime.NewFatalResult(fmt.Errorf("cannot get composite: %w", err)) } - if comp.Spec.Parameters.Service.FQDN == "" { - return nil + if len(comp.Spec.Parameters.Service.FQDN) == 0 { + return runtime.NewFatalResult(fmt.Errorf("FQDN array is empty, but requires at least one entry, %w", errors.New("empty fqdn"))) } values, err := common.GetDesiredReleaseValues(svc, comp.GetName()+"-release") @@ -64,9 +65,7 @@ func enableIngresValues(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNNextcloud, "path": comp.Spec.Parameters.Service.RelativePath, "tls": []map[string]any{ { - "hosts": []string{ - fqdn, - }, + "hosts": fqdn, "secretName": "nextcloud-ingress-cert", }, }, diff --git a/test/functions/vshnnextcloud/01_default.yaml b/test/functions/vshnnextcloud/01_default.yaml index b80fe493cf..d9643d2db4 100644 --- a/test/functions/vshnnextcloud/01_default.yaml +++ b/test/functions/vshnnextcloud/01_default.yaml @@ -41,6 +41,8 @@ observed: spec: parameters: service: + fqdn: + - nextcloud.example.com useExternalPostgreSQL: true version: "29" claimRef: @@ -114,6 +116,8 @@ observed: spec: parameters: service: + fqdn: + - nextcloud.example.com repackEnabled: false vacuumEnabled: false backup: diff --git a/test/functions/vshnnextcloud/01_no_fqdn.yaml b/test/functions/vshnnextcloud/01_no_fqdn.yaml new file mode 100644 index 0000000000..b80fe493cf --- /dev/null +++ b/test/functions/vshnnextcloud/01_no_fqdn.yaml @@ -0,0 +1,183 @@ +desired: {} +input: + apiVersion: v1 + kind: ConfigMap + metadata: + annotations: {} + labels: + name: xfn-config + name: xfn-config + data: + defaultPlan: standard-2 + collabora_image: docker.io/collabora/code:24.04.9.2.1 + collaboraCPULimit: "1" + collaboraCPURequests: 250m + collaboraMemoryLimit: 1Gi + collaboraMemoryRequests: 256Mi + imageTag: nextcloud_collabora + controlNamespace: appcat-control + plans: '{"standard-2": {"size": {"cpu": "500m", "disk": "16Gi", "enabled": + true, "memory": "2Gi"}}, "standard-4": {"size": {"cpu": "1", "disk": "16Gi", + "enabled": true, "memory": "4Gi"}}, "standard-8": {"size": {"cpu": "2", + "disk": "16Gi", "enabled": true, "memory": "8Gi"}}}' + isOpenshift: "false" +observed: + composite: + resource: + apiVersion: vshn.appcat.vshn.io/v1 + kind: XVSHNNextcloud + metadata: + annotations: null + creationTimestamp: "2024-07-08T16:30:28Z" + finalizers: + - composite.apiextensions.crossplane.io + generateName: nextcloud- + generation: 1 + labels: + crossplane.io/claim-name: nextcloud + crossplane.io/claim-namespace: unit-test + crossplane.io/composite: nextcloud-gc9x4 + name: nextcloud-gc9x4 + spec: + parameters: + service: + useExternalPostgreSQL: true + version: "29" + claimRef: + apiVersion: vshn.appcat.vshn.io/v1 + kind: VSHNNextcloud + name: nextcloud + namespace: unit-test + compositionRef: + name: vshnnextcloud.vshn.appcat.vshn.io + compositionRevisionRef: + name: vshnnextcloud.vshn.appcat.vshn.io-ce52f13 + compositionUpdatePolicy: Automatic + resources: + # necessary for collabora tests + nextcloud-gc9x4-release: + resource: + apiVersion: helm.crossplane.io/v1beta1 + kind: Release + metadata: + name: nextcloud-gc9x4 + spec: {} + status: + atProvider: + conditions: + - lastTransitionTime: "2024-07-08T14:50:19Z" + reason: Available + status: "True" + type: Ready + - lastTransitionTime: "2024-07-08T14:50:18Z" + reason: ReconcileSuccess + status: "True" + type: Synced + nextcloud-gc9x4-claim-ns-observer: + resource: + apiVersion: v1 + kind: Object + metadata: + name: unit-test + spec: + forProvider: + manifest: + apiVersion: v1 + kind: Namespace + metadata: + name: unit-test + status: + atProvider: + manifest: + apiVersion: v1 + kind: Namespace + metadata: + name: unit-test + labels: + appuio.io/organization: vshn + nextcloud-gc9x4-credentials-secret: + connection_details: + admin: czNjcjN0UGFzcwo= + nextcloud-gc9x4-pg: + resource: + apiVersion: v1 + kind: Object + metadata: + name: "nextcloud-gc9x4-pg" + spec: + forProvider: + manifest: + apiVersion: vshn.appcat.vshn.io/v1 + kind: VSHNNextcloud + metadata: + name: "nextcloud-gc9x4-pg" + spec: + parameters: + service: + repackEnabled: false + vacuumEnabled: false + backup: + retention: 6 + deletionProtection: true + deletionRetention: 7 + encryption: + enabled: false + instances: 1 + security: + allowAllNamespaces: false + status: + atProvider: + manifest: + apiVersion: vshn.appcat.vshn.io/v1 + kind: VSHNNextcloud + metadata: + annotations: null + creationTimestamp: "2024-07-08T16:30:28Z" + generation: 8 + name: "nextcloud-gc9x4-pg" + namespace: vshn-postgresql-nextcloud-gc9x4 + resourceVersion: "583272583" + uid: 44ead047-98de-4e73-9cc0-d99454090a36 + spec: + parameters: + service: + repackEnabled: false + vacuumEnabled: false + backup: + retention: 6 + deletionProtection: true + deletionRetention: 7 + encryption: + enabled: false + instances: 1 + security: + allowAllNamespaces: false + status: + conditions: + - lastTransitionTime: "2024-07-08T14:50:19Z" + reason: Available + status: "True" + type: Ready + - lastTransitionTime: "2024-07-08T14:50:18Z" + reason: ReconcileSuccess + status: "True" + type: Synced + connection_details: + POSTGRESQL_HOST: bmV4dGNsb3VkLWdjOXg0LnZzaG4tcG9zdGdyZXNxbC1uZXh0Y2xvdWQtZ2N4NC5zdmMuY2x1c3Rlci5sb2NhbAo= + POSTGRESQL_DB: cG9zdGdyZXMK + POSTGRESQL_URL: cG9zdGdyZXM6Ly9wb3N0Z3Jlczo0YTcwLWY2OTItNGE5YS1iMzBAbmV4dGNsb3VkLWdjOXg0LnZzaG4tcG9zdGdyZXNxbC1uZXh0Y2xvdWQtZ2N4NC5zdmMuY2x1c3Rlci5sb2NhbC9wb3N0Z3Jlcwo= + POSTGRESQL_USER: cG9zdGdyZXMK + POSTGRESQL_PORT: NTQzMgo= + POSTGRESQL_PASSWORD: NGE3MC1mNjkyLTRhOWEtYjMwCg== + status: + conditions: + - lastTransitionTime: "2024-07-08T14:50:19Z" + reason: Available + status: "True" + type: Ready + - lastTransitionTime: "2024-07-08T14:50:18Z" + reason: ReconcileSuccess + status: "True" + type: Synced + + diff --git a/test/functions/vshnnextcloud/02_no_pg.yaml b/test/functions/vshnnextcloud/02_no_pg.yaml index 097add75ca..4bd997599d 100644 --- a/test/functions/vshnnextcloud/02_no_pg.yaml +++ b/test/functions/vshnnextcloud/02_no_pg.yaml @@ -35,6 +35,8 @@ observed: spec: parameters: service: + fqdn: + - nextcloud.example.com useExternalPostgreSQL: false version: "29" claimRef: diff --git a/test/functions/vshnnextcloud/02_withFQDN.yaml b/test/functions/vshnnextcloud/02_withFQDN.yaml index 4e63052f32..3ccdeca341 100644 --- a/test/functions/vshnnextcloud/02_withFQDN.yaml +++ b/test/functions/vshnnextcloud/02_withFQDN.yaml @@ -32,7 +32,8 @@ observed: spec: parameters: service: - fqdn: example.com + fqdn: + - example.com resources: mynextcloud-pg: connection_details: From 7b257076a1aa3774f7ee7149d91ff0cd3d22a693 Mon Sep 17 00:00:00 2001 From: Nicolas Bigler Date: Thu, 21 Nov 2024 16:20:40 +0100 Subject: [PATCH 2/5] Fix ingress Signed-off-by: Nicolas Bigler --- .../functions/vshnnextcloud/ingress.go | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/pkg/comp-functions/functions/vshnnextcloud/ingress.go b/pkg/comp-functions/functions/vshnnextcloud/ingress.go index 04bb36c052..b6d5636727 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/ingress.go +++ b/pkg/comp-functions/functions/vshnnextcloud/ingress.go @@ -2,17 +2,16 @@ package vshnnextcloud import ( "context" - "encoding/json" "errors" "fmt" xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" - xhelmv1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" - "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" "gopkg.in/yaml.v2" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) // AddIngress adds an inrgess to the Nextcloud instance. @@ -27,63 +26,67 @@ func AddIngress(_ context.Context, comp *vshnv1.VSHNNextcloud, svc *runtime.Serv return runtime.NewFatalResult(fmt.Errorf("FQDN array is empty, but requires at least one entry, %w", errors.New("empty fqdn"))) } - values, err := common.GetDesiredReleaseValues(svc, comp.GetName()+"-release") - if err != nil { - return runtime.NewWarningResult(fmt.Sprintf("cannot get desired release values: %s", err)) + annotations := map[string]string{} + if svc.Config.Data["ingress_annotations"] != "" { + err := yaml.Unmarshal([]byte(svc.Config.Data["ingress_annotations"]), annotations) + if err != nil { + svc.Log.Error(err, "cannot unmarshal ingress annotations from input") + svc.AddResult(runtime.NewWarningResult(fmt.Sprintf("cannot unmarshal ingress annotations from input: %s", err))) + } } - svc.Log.Info("Enable ingress for release") - enableIngresValues(svc, comp, values) - - release := &xhelmv1.Release{} - err = svc.GetDesiredComposedResourceByName(release, comp.GetName()+"-release") - if err != nil { - return runtime.NewWarningResult(fmt.Sprintf("cannot get desired release: %s", err)) + ingress := &netv1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: comp.GetName(), + Namespace: comp.GetInstanceNamespace(), + Annotations: annotations, + }, + Spec: netv1.IngressSpec{ + Rules: createIngressRule(comp), + TLS: []netv1.IngressTLS{ + { + Hosts: comp.Spec.Parameters.Service.FQDN, + SecretName: "nextcloud-ingress-cert", + }, + }, + }, } - vb, err := json.Marshal(values) + err = svc.SetDesiredKubeObject(ingress, comp.GetName()+"-ingress") if err != nil { - return runtime.NewWarningResult(fmt.Sprintf("cannot marhal values: %s", err)) - } - - release.Spec.ForProvider.Values.Raw = vb - - err = svc.SetDesiredComposedResourceWithName(release, comp.GetName()+"-release") - if err != nil { - return runtime.NewWarningResult(fmt.Sprintf("cannot set desired release: %s", err)) + return runtime.NewWarningResult(fmt.Sprintf("cannot set create ingress: %s", err)) } return nil } -func enableIngresValues(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNNextcloud, values map[string]any) { - fqdn := comp.Spec.Parameters.Service.FQDN - - values["ingress"] = map[string]any{ - "enabled": true, - "servicePort": "http", - "path": comp.Spec.Parameters.Service.RelativePath, - "tls": []map[string]any{ - { - "hosts": fqdn, - "secretName": "nextcloud-ingress-cert", +func createIngressRule(comp *vshnv1.VSHNNextcloud) []netv1.IngressRule { + + ingressRules := []netv1.IngressRule{} + + for _, fqdn := range comp.Spec.Parameters.Service.FQDN { + rule := netv1.IngressRule{ + Host: fqdn, + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{ + { + Path: "/", + PathType: ptr.To(netv1.PathType("Prefix")), + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: comp.GetName(), + Port: netv1.ServiceBackendPort{ + Number: 8080, + }, + }, + }, + }, + }, + }, }, - }, - } - - if svc.Config.Data["ingress_annotations"] != "" { - annotations := map[string]any{} - - err := yaml.Unmarshal([]byte(svc.Config.Data["ingress_annotations"]), annotations) - if err != nil { - svc.Log.Error(err, "cannot unmarshal ingress annotations from input") - svc.AddResult(runtime.NewWarningResult(fmt.Sprintf("cannot unmarshal ingress annotations from input: %s", err))) - } - - err = unstructured.SetNestedMap(values, annotations, "ingress", "annotations") - if err != nil { - svc.Log.Error(err, "cannot set ingress annotations") - svc.AddResult(runtime.NewWarningResult(fmt.Sprintf("cannot set ingress annotations: %s", err))) } + ingressRules = append(ingressRules, rule) } + return ingressRules } From a1628dd4b2bd0551894d8c1fd6c57400deff7442 Mon Sep 17 00:00:00 2001 From: Nicolas Bigler Date: Tue, 26 Nov 2024 11:06:07 +0100 Subject: [PATCH 3/5] Apply suggestions Signed-off-by: Nicolas Bigler --- apis/vshn/v1/vshn_nextcloud.go | 2 +- .../functions/vshnnextcloud/deploy.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apis/vshn/v1/vshn_nextcloud.go b/apis/vshn/v1/vshn_nextcloud.go index 8c7ef4410f..9dc34ba773 100644 --- a/apis/vshn/v1/vshn_nextcloud.go +++ b/apis/vshn/v1/vshn_nextcloud.go @@ -93,7 +93,7 @@ type VSHNNextcloudServiceSpec struct { // +kubebuilder:validation:Required // +kubebuilder:validation:MinItems=1 - // FQDN contains the FQDNs which will be used for the ingress. + // FQDN contains the FQDNs array, which will be used for the ingress. // If it's not set, no ingress will be deployed. // This also enables strict hostname checking for this FQDN. FQDN []string `json:"fqdn"` diff --git a/pkg/comp-functions/functions/vshnnextcloud/deploy.go b/pkg/comp-functions/functions/vshnnextcloud/deploy.go index 34fd51c347..9653360fb4 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/deploy.go +++ b/pkg/comp-functions/functions/vshnnextcloud/deploy.go @@ -9,6 +9,7 @@ import ( "strings" "time" + valid "github.com/asaskevich/govalidator" xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" xhelmv1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" @@ -260,6 +261,10 @@ func newValues(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.VS return nil, fmt.Errorf("FQDN array is empty, but requires at least one entry: %w", errors.New("empty fqdn")) } + if err := validateFQDNs(comp.Spec.Parameters.Service.FQDN); err != nil { + return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + } + trustedDomain := []string{ comp.GetName() + "." + comp.GetInstanceNamespace() + ".svc.cluster.local", } @@ -438,3 +443,12 @@ func setBackgroundJobMaintenance(t vshnv1.TimeOfDay, nextcloudConfig string) str backgroundJobHour := t.GetTime().Add(40 * time.Minute).Add(time.Hour).Hour() return strings.Replace(nextcloudConfig, "%maintenance_value%", strconv.Itoa(backgroundJobHour), 1) } + +func validateFQDNs(fqdns []string) error { + for _, fqdn := range fqdns { + if !valid.IsDNSName(fqdn) { + return fmt.Errorf("FQDN %s is not a valid DNS name", fqdn) + } + } + return nil +} From 4990d7c61671682d09c005a65abca926dd18b4d9 Mon Sep 17 00:00:00 2001 From: Nicolas Bigler Date: Tue, 26 Nov 2024 13:26:32 +0100 Subject: [PATCH 4/5] Add webhook validation for FQDN Signed-off-by: Nicolas Bigler --- crds/vshn.appcat.vshn.io_vshnnextclouds.yaml | 2 +- crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml | 2 +- .../functions/vshnnextcloud/collabora.go | 5 -- .../functions/vshnnextcloud/collabora_test.go | 7 -- .../functions/vshnnextcloud/deploy.go | 8 -- .../functions/vshnnextcloud/deploy_test.go | 11 --- .../webhooks/default_handler_test.go | 4 +- pkg/controller/webhooks/nextcloud.go | 78 ++++++++++++++++++- 8 files changed, 79 insertions(+), 38 deletions(-) diff --git a/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml b/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml index 28a2a0843a..d5d36f1fe5 100644 --- a/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml +++ b/crds/vshn.appcat.vshn.io_vshnnextclouds.yaml @@ -4866,7 +4866,7 @@ spec: type: object fqdn: description: |- - FQDN contains the FQDNs which will be used for the ingress. + FQDN contains the FQDNs array, which will be used for the ingress. If it's not set, no ingress will be deployed. This also enables strict hostname checking for this FQDN. items: diff --git a/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml b/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml index 69ee177683..ace2706305 100644 --- a/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml +++ b/crds/vshn.appcat.vshn.io_xvshnnextclouds.yaml @@ -5595,7 +5595,7 @@ spec: type: object fqdn: description: |- - FQDN contains the FQDNs which will be used for the ingress. + FQDN contains the FQDNs array, which will be used for the ingress. If it's not set, no ingress will be deployed. This also enables strict hostname checking for this FQDN. items: diff --git a/pkg/comp-functions/functions/vshnnextcloud/collabora.go b/pkg/comp-functions/functions/vshnnextcloud/collabora.go index f6dfad8ac9..07b03ff2ef 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/collabora.go +++ b/pkg/comp-functions/functions/vshnnextcloud/collabora.go @@ -10,7 +10,6 @@ import ( "encoding/pem" "fmt" - valid "github.com/asaskevich/govalidator" cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" @@ -53,10 +52,6 @@ func DeployCollabora(ctx context.Context, comp *vshnv1.VSHNNextcloud, svc *runti return runtime.NewNormalResult("Collabora not enabled") } - if !valid.IsDNSName(comp.Spec.Parameters.Service.Collabora.FQDN) { - return runtime.NewWarningResult("Collabora FQDN is not a valid DNS name: " + comp.Spec.Parameters.Service.Collabora.FQDN) - } - rules := []rbacv1.PolicyRule{ { APIGroups: []string{""}, diff --git a/pkg/comp-functions/functions/vshnnextcloud/collabora_test.go b/pkg/comp-functions/functions/vshnnextcloud/collabora_test.go index 60ad88d8ea..5b4e87fc0f 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/collabora_test.go +++ b/pkg/comp-functions/functions/vshnnextcloud/collabora_test.go @@ -56,11 +56,4 @@ func Test_addCollabora(t *testing.T) { res = DeployCollabora(ctx, comp, svc) assert.True(t, res.Severity == v1beta1.Severity_SEVERITY_NORMAL && res.Message == "Collabora not enabled") - - comp.Spec.Parameters.Service.Collabora.Enabled = true - comp.Spec.Parameters.Service.Collabora.FQDN = "" - res = DeployCollabora(ctx, comp, svc) - - assert.True(t, res.Severity == v1beta1.Severity_SEVERITY_WARNING && res.Message == "Collabora FQDN is not a valid DNS name: "+comp.Spec.Parameters.Service.Collabora.FQDN) - } diff --git a/pkg/comp-functions/functions/vshnnextcloud/deploy.go b/pkg/comp-functions/functions/vshnnextcloud/deploy.go index 9653360fb4..6cfb0bb38a 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/deploy.go +++ b/pkg/comp-functions/functions/vshnnextcloud/deploy.go @@ -257,14 +257,6 @@ func newValues(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.VS } } - if len(comp.Spec.Parameters.Service.FQDN) == 0 { - return nil, fmt.Errorf("FQDN array is empty, but requires at least one entry: %w", errors.New("empty fqdn")) - } - - if err := validateFQDNs(comp.Spec.Parameters.Service.FQDN); err != nil { - return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) - } - trustedDomain := []string{ comp.GetName() + "." + comp.GetInstanceNamespace() + ".svc.cluster.local", } diff --git a/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go b/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go index bdb234a8a5..7af08c5bdb 100644 --- a/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go +++ b/pkg/comp-functions/functions/vshnnextcloud/deploy_test.go @@ -4,8 +4,6 @@ import ( "context" _ "embed" "encoding/json" - "errors" - "fmt" "strings" "testing" @@ -38,15 +36,6 @@ func Test_addNextcloud(t *testing.T) { } -func Test_addNextcloud_no_fqdn(t *testing.T) { - svc, _ := getNextcloudComp(t, "vshnnextcloud/01_no_fqdn.yaml") - - ctx := context.TODO() - - assert.Equal(t, DeployNextcloud(ctx, &vshnv1.VSHNNextcloud{}, svc), runtime.NewWarningResult(fmt.Sprintf("cannot create release: FQDN array is empty, but requires at least one entry: %s", errors.New("empty fqdn")))) - -} - func Test_addReleaseInternalDB(t *testing.T) { svc, comp := getNextcloudComp(t, "vshnnextcloud/02_no_pg.yaml") diff --git a/pkg/controller/webhooks/default_handler_test.go b/pkg/controller/webhooks/default_handler_test.go index e4e5f2c1af..ef48a83688 100644 --- a/pkg/controller/webhooks/default_handler_test.go +++ b/pkg/controller/webhooks/default_handler_test.go @@ -32,7 +32,7 @@ func TestSetupWebhookHandlerWithManager_ValidateCreate(t *testing.T) { WithObjects(claimNS). Build() - handler := TestWebhookHandler{ + handler := NextcloudWebhookHandler{ DefaultWebhookHandler: DefaultWebhookHandler{ client: fclient, log: logr.Discard(), @@ -146,7 +146,7 @@ func TestSetupWebhookHandlerWithManager_ValidateDelete(t *testing.T) { WithObjects(claimNS). Build() - handler := TestWebhookHandler{ + handler := NextcloudWebhookHandler{ DefaultWebhookHandler: DefaultWebhookHandler{ client: fclient, log: logr.Discard(), diff --git a/pkg/controller/webhooks/nextcloud.go b/pkg/controller/webhooks/nextcloud.go index a9a6ec7d9e..49be9dda3d 100644 --- a/pkg/controller/webhooks/nextcloud.go +++ b/pkg/controller/webhooks/nextcloud.go @@ -1,10 +1,17 @@ package webhooks import ( + "context" + "errors" + "fmt" + + valid "github.com/asaskevich/govalidator" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) //+kubebuilder:webhook:verbs=create;update;delete,path=/validate-vshn-appcat-vshn-io-v1-vshnnextcloud,mutating=false,failurePolicy=fail,groups=vshn.appcat.vshn.io,resources=vshnnextclouds,versions=v1,name=vshnnextcloud.vshn.appcat.vshn.io,sideEffects=None,admissionReviewVersions=v1 @@ -17,9 +24,9 @@ var ( nextcloudGR = schema.GroupResource{Group: nextcloudGK.Group, Resource: "vshnnextcloud"} ) -var _ webhook.CustomValidator = &TestWebhookHandler{} +var _ webhook.CustomValidator = &NextcloudWebhookHandler{} -type TestWebhookHandler struct { +type NextcloudWebhookHandler struct { DefaultWebhookHandler } @@ -28,7 +35,7 @@ func SetupNextcloudWebhookHandlerWithManager(mgr ctrl.Manager, withQuota bool) e return ctrl.NewWebhookManagedBy(mgr). For(&vshnv1.VSHNNextcloud{}). - WithValidator(&TestWebhookHandler{ + WithValidator(&NextcloudWebhookHandler{ DefaultWebhookHandler: *New( mgr.GetClient(), mgr.GetLogger().WithName("webhook").WithName("nextcloud"), @@ -41,3 +48,68 @@ func SetupNextcloudWebhookHandlerWithManager(mgr ctrl.Manager, withQuota bool) e }). Complete() } + +func (n *NextcloudWebhookHandler) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + warning, err := n.DefaultWebhookHandler.ValidateCreate(ctx, obj) + if warning != nil || err != nil { + return warning, err + } + + nx, ok := obj.(*vshnv1.VSHNNextcloud) + if !ok { + return nil, fmt.Errorf("provided manifest is not a valid VSHNPostgreSQL object") + } + + if len(nx.Spec.Parameters.Service.FQDN) == 0 { + return nil, fmt.Errorf("FQDN array is empty, but requires at least one entry: %w", errors.New("empty fqdn")) + } + + if err := validateFQDNs(nx.Spec.Parameters.Service.FQDN); err != nil { + return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + } + + if nx.Spec.Parameters.Service.Collabora.Enabled { + if err := validateFQDNs([]string{nx.Spec.Parameters.Service.Collabora.FQDN}); err != nil { + return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + } + } + + return nil, nil +} + +func (n *NextcloudWebhookHandler) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + warning, err := n.DefaultWebhookHandler.ValidateUpdate(ctx, oldObj, newObj) + if warning != nil || err != nil { + return warning, err + } + + nx, ok := newObj.(*vshnv1.VSHNNextcloud) + if !ok { + return nil, fmt.Errorf("provided manifest is not a valid VSHNPostgreSQL object") + } + + if len(nx.Spec.Parameters.Service.FQDN) == 0 { + return nil, fmt.Errorf("FQDN array is empty, but requires at least one entry: %w", errors.New("empty fqdn")) + } + + if err := validateFQDNs(nx.Spec.Parameters.Service.FQDN); err != nil { + return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + } + + if nx.Spec.Parameters.Service.Collabora.Enabled { + if err := validateFQDNs([]string{nx.Spec.Parameters.Service.Collabora.FQDN}); err != nil { + return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + } + } + + return nil, nil +} + +func validateFQDNs(fqdns []string) error { + for _, fqdn := range fqdns { + if !valid.IsDNSName(fqdn) { + return fmt.Errorf("FQDN %s is not a valid DNS name", fqdn) + } + } + return nil +} From 3ac924eb83b7b534f42431e858088c0bdf0ef24d Mon Sep 17 00:00:00 2001 From: Nicolas Bigler Date: Tue, 26 Nov 2024 15:08:13 +0100 Subject: [PATCH 5/5] Add tests for the nextcloud webhooks Signed-off-by: Nicolas Bigler --- .../webhooks/default_handler_test.go | 94 +++++------ pkg/controller/webhooks/nextcloud.go | 17 +- pkg/controller/webhooks/nextcloud_test.go | 148 ++++++++++++++++++ 3 files changed, 199 insertions(+), 60 deletions(-) create mode 100644 pkg/controller/webhooks/nextcloud_test.go diff --git a/pkg/controller/webhooks/default_handler_test.go b/pkg/controller/webhooks/default_handler_test.go index ef48a83688..9f9102b4a7 100644 --- a/pkg/controller/webhooks/default_handler_test.go +++ b/pkg/controller/webhooks/default_handler_test.go @@ -32,23 +32,23 @@ func TestSetupWebhookHandlerWithManager_ValidateCreate(t *testing.T) { WithObjects(claimNS). Build() - handler := NextcloudWebhookHandler{ + handler := KeycloakWebhookHandler{ DefaultWebhookHandler: DefaultWebhookHandler{ client: fclient, log: logr.Discard(), withQuota: true, - obj: &vshnv1.VSHNNextcloud{}, - name: "nextcloud", + obj: &vshnv1.VSHNKeycloak{}, + name: "keycloak", }, } - redisOrig := &vshnv1.VSHNNextcloud{ + keycloakOrig := &vshnv1.VSHNKeycloak{ ObjectMeta: metav1.ObjectMeta{ Name: "myinstance", Namespace: "claimns", }, - Spec: vshnv1.VSHNNextcloudSpec{ - Parameters: vshnv1.VSHNNextcloudParameters{ + Spec: vshnv1.VSHNKeycloakSpec{ + Parameters: vshnv1.VSHNKeycloakParameters{ Size: vshnv1.VSHNSizeSpec{ Requests: vshnv1.VSHNDBaaSSizeRequestsSpec{ CPU: "500m", @@ -59,71 +59,71 @@ func TestSetupWebhookHandlerWithManager_ValidateCreate(t *testing.T) { } // When within quota - _, err := handler.ValidateCreate(ctx, redisOrig) + _, err := handler.ValidateCreate(ctx, keycloakOrig) //Then no err assert.NoError(t, err) // When quota breached // CPU Requests - nextcloudInvalid := redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Requests.CPU = "5000m" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid := keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Requests.CPU = "5000m" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // CPU Limit - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.CPU = "5000m" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.CPU = "5000m" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // Memory Limit - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Memory = "25Gi" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Memory = "25Gi" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // Memory Requests - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Requests.Memory = "25Gi" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Requests.Memory = "25Gi" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // Disk - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Disk = "25Ti" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Disk = "25Ti" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) //When invalid size // CPU Requests - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Requests.CPU = "foo" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Requests.CPU = "foo" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // CPU Limit - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.CPU = "foo" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.CPU = "foo" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // Memory Limit - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Memory = "foo" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Memory = "foo" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // Memory Requests - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Requests.Memory = "foo" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Requests.Memory = "foo" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) // Disk - nextcloudInvalid = redisOrig.DeepCopy() - nextcloudInvalid.Spec.Parameters.Size.Disk = "foo" - _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + keycloakInvalid = keycloakOrig.DeepCopy() + keycloakInvalid.Spec.Parameters.Size.Disk = "foo" + _, err = handler.ValidateCreate(ctx, keycloakInvalid) assert.Error(t, err) } @@ -146,23 +146,23 @@ func TestSetupWebhookHandlerWithManager_ValidateDelete(t *testing.T) { WithObjects(claimNS). Build() - handler := NextcloudWebhookHandler{ + handler := KeycloakWebhookHandler{ DefaultWebhookHandler: DefaultWebhookHandler{ client: fclient, log: logr.Discard(), withQuota: true, - obj: &vshnv1.VSHNNextcloud{}, - name: "nextcloud", + obj: &vshnv1.VSHNKeycloak{}, + name: "keycloak", }, } - nextcloudOrig := &vshnv1.VSHNNextcloud{ + keycloakOrig := &vshnv1.VSHNKeycloak{ ObjectMeta: metav1.ObjectMeta{ Name: "myinstance", Namespace: "claimns", }, - Spec: vshnv1.VSHNNextcloudSpec{ - Parameters: vshnv1.VSHNNextcloudParameters{ + Spec: vshnv1.VSHNKeycloakSpec{ + Parameters: vshnv1.VSHNKeycloakParameters{ Security: vshnv1.Security{ DeletionProtection: true, }, @@ -171,16 +171,16 @@ func TestSetupWebhookHandlerWithManager_ValidateDelete(t *testing.T) { } // When within quota - _, err := handler.ValidateDelete(ctx, nextcloudOrig) + _, err := handler.ValidateDelete(ctx, keycloakOrig) //Then err assert.Error(t, err) //Instances - nextcloudDeletable := nextcloudOrig.DeepCopy() - nextcloudDeletable.Spec.Parameters.Security.DeletionProtection = false + keycloakDeletable := keycloakOrig.DeepCopy() + keycloakDeletable.Spec.Parameters.Security.DeletionProtection = false - _, err = handler.ValidateDelete(ctx, nextcloudDeletable) + _, err = handler.ValidateDelete(ctx, keycloakDeletable) //Then no err assert.NoError(t, err) diff --git a/pkg/controller/webhooks/nextcloud.go b/pkg/controller/webhooks/nextcloud.go index 49be9dda3d..8db99ab353 100644 --- a/pkg/controller/webhooks/nextcloud.go +++ b/pkg/controller/webhooks/nextcloud.go @@ -2,7 +2,6 @@ package webhooks import ( "context" - "errors" "fmt" valid "github.com/asaskevich/govalidator" @@ -60,17 +59,13 @@ func (n *NextcloudWebhookHandler) ValidateCreate(ctx context.Context, obj runtim return nil, fmt.Errorf("provided manifest is not a valid VSHNPostgreSQL object") } - if len(nx.Spec.Parameters.Service.FQDN) == 0 { - return nil, fmt.Errorf("FQDN array is empty, but requires at least one entry: %w", errors.New("empty fqdn")) - } - if err := validateFQDNs(nx.Spec.Parameters.Service.FQDN); err != nil { - return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + return nil, err } if nx.Spec.Parameters.Service.Collabora.Enabled { if err := validateFQDNs([]string{nx.Spec.Parameters.Service.Collabora.FQDN}); err != nil { - return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + return nil, err } } @@ -88,17 +83,13 @@ func (n *NextcloudWebhookHandler) ValidateUpdate(ctx context.Context, oldObj, ne return nil, fmt.Errorf("provided manifest is not a valid VSHNPostgreSQL object") } - if len(nx.Spec.Parameters.Service.FQDN) == 0 { - return nil, fmt.Errorf("FQDN array is empty, but requires at least one entry: %w", errors.New("empty fqdn")) - } - if err := validateFQDNs(nx.Spec.Parameters.Service.FQDN); err != nil { - return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + return nil, err } if nx.Spec.Parameters.Service.Collabora.Enabled { if err := validateFQDNs([]string{nx.Spec.Parameters.Service.Collabora.FQDN}); err != nil { - return nil, fmt.Errorf("FQDN is not a valid DNS name: %w", err) + return nil, err } } diff --git a/pkg/controller/webhooks/nextcloud_test.go b/pkg/controller/webhooks/nextcloud_test.go new file mode 100644 index 0000000000..e9778e5642 --- /dev/null +++ b/pkg/controller/webhooks/nextcloud_test.go @@ -0,0 +1,148 @@ +package webhooks + +import ( + "context" + "fmt" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + "github.com/vshn/appcat/v4/pkg" + "github.com/vshn/appcat/v4/pkg/common/utils" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestWebhookHandlerWithManager_ValidateCreate_FQDN(t *testing.T) { + // Given + claimNS := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "claimns", + Labels: map[string]string{ + utils.OrgLabelName: "myorg", + }, + }, + } + + ctx := context.TODO() + + fclient := fake.NewClientBuilder(). + WithScheme(pkg.SetupScheme()). + WithObjects(claimNS). + Build() + + handler := NextcloudWebhookHandler{ + DefaultWebhookHandler: DefaultWebhookHandler{ + client: fclient, + log: logr.Discard(), + withQuota: true, + obj: &vshnv1.VSHNNextcloud{}, + name: "nextcloud", + }, + } + + nextcloudOrig := &vshnv1.VSHNNextcloud{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myinstance", + Namespace: "claimns", + }, + Spec: vshnv1.VSHNNextcloudSpec{ + Parameters: vshnv1.VSHNNextcloudParameters{ + Service: vshnv1.VSHNNextcloudServiceSpec{ + FQDN: []string{ + "mynextcloud.example.tld", + }, + }, + Size: vshnv1.VSHNSizeSpec{ + Requests: vshnv1.VSHNDBaaSSizeRequestsSpec{ + CPU: "500m", + }, + }, + }, + }, + } + + _, err := handler.ValidateCreate(ctx, nextcloudOrig) + + //Then no err + assert.NoError(t, err) + + // When FQDN invalid + nextcloudInvalid := nextcloudOrig.DeepCopy() + nextcloudInvalid.Spec.Parameters.Service.FQDN = []string{ + "n€xtcloud.example.tld", + } + + _, err = handler.ValidateCreate(ctx, nextcloudInvalid) + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("FQDN n€xtcloud.example.tld is not a valid DNS name"), err) +} + +func TestWebhookHandlerWithManager_ValidateUpdate_FQDN(t *testing.T) { + // Given + claimNS := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "claimns", + Labels: map[string]string{ + utils.OrgLabelName: "myorg", + }, + }, + } + + ctx := context.TODO() + + fclient := fake.NewClientBuilder(). + WithScheme(pkg.SetupScheme()). + WithObjects(claimNS). + Build() + + handler := NextcloudWebhookHandler{ + DefaultWebhookHandler: DefaultWebhookHandler{ + client: fclient, + log: logr.Discard(), + withQuota: true, + obj: &vshnv1.VSHNNextcloud{}, + name: "nextcloud", + }, + } + + nextcloudOrig := &vshnv1.VSHNNextcloud{ + ObjectMeta: metav1.ObjectMeta{ + Name: "myinstance", + Namespace: "claimns", + }, + Spec: vshnv1.VSHNNextcloudSpec{ + Parameters: vshnv1.VSHNNextcloudParameters{ + Service: vshnv1.VSHNNextcloudServiceSpec{ + FQDN: []string{ + "mynextcloud.example.tld", + }, + }, + Size: vshnv1.VSHNSizeSpec{ + Requests: vshnv1.VSHNDBaaSSizeRequestsSpec{ + CPU: "500m", + }, + }, + }, + }, + } + nextcloudNew := nextcloudOrig.DeepCopy() + nextcloudNew.Spec.Parameters.Service.FQDN = append(nextcloudNew.Spec.Parameters.Service.FQDN, "myother-nexctloud.example.tld") + + _, err := handler.ValidateUpdate(ctx, nextcloudOrig, nextcloudNew) + + //Then no err + assert.NoError(t, err) + + // When FQDN invalid + nextcloudInvalid := nextcloudOrig.DeepCopy() + nextcloudInvalid.Spec.Parameters.Service.FQDN = []string{ + "n€xtcloud.example.tld", + } + + _, err = handler.ValidateUpdate(ctx, nextcloudOrig, nextcloudInvalid) + assert.Error(t, err) + assert.Equal(t, fmt.Errorf("FQDN n€xtcloud.example.tld is not a valid DNS name"), err) +}