From c13bf7ce9f18b63e6a57614872ee06838cdac00d Mon Sep 17 00:00:00 2001 From: Jelle Dijkstra Date: Mon, 22 Dec 2025 16:07:51 +0100 Subject: [PATCH 1/7] (chore) initial scaffold from release version: v4.5.1 --- .devcontainer/devcontainer.json | 2 +- .github/workflows/build-and-publish-image.yml | 67 - .github/workflows/lint.yml | 6 +- .github/workflows/test-e2e.yml | 4 +- .github/workflows/test.yml | 4 +- .golangci.yml | 143 +- DEVELOPMENT.md | 5 - Dockerfile | 6 +- LICENSE | 21 - Makefile | 11 +- README.md | 107 +- api/v2beta1/groupversion_info.go | 34 +- api/v2beta1/shared_conversion.go | 289 --- api/v2beta1/shared_types.go | 145 -- api/v2beta1/wfs_conversion.go | 269 +-- api/v2beta1/wfs_types.go | 87 +- api/v2beta1/wms_conversion.go | 673 +------ api/v2beta1/wms_conversion_test.go | 232 --- api/v2beta1/wms_types.go | 127 +- api/v2beta1/zz_generated.deepcopy.go | 796 +------- api/v3/groupversion_info.go | 45 +- api/v3/shared_types.go | 329 --- api/v3/shared_validation.go | 210 -- api/v3/wfs_conversion.go | 28 +- api/v3/wfs_types.go | 309 +-- api/v3/wfs_validation.go | 101 - api/v3/wms_conversion.go | 28 +- api/v3/wms_types.go | 697 +------ api/v3/wms_types_test.go | 166 -- api/v3/wms_validation.go | 334 --- api/v3/zz_generated.deepcopy.go | 883 +------- build-push-deploy-locally.sh | 36 - cmd/main.go | 220 +- config/crd/bases/embed.go | 51 - config/crd/bases/pdok.nl_wfs.yaml | 1185 ----------- config/crd/bases/pdok.nl_wfses.yaml | 92 + config/crd/bases/pdok.nl_wms.yaml | 1782 ----------------- config/crd/bases/pdok.nl_wmses.yaml | 92 + config/crd/kustomization.yaml | 6 +- config/crd/update_openapi.go | 180 -- config/default/kustomization.yaml | 406 ++-- config/manager/kustomization.yaml | 6 - config/manager/manager.yaml | 2 - config/prometheus/monitor_tls_patch.yaml | 37 +- config/rbac/role.yaml | 99 - config/samples/samples.go | 17 - config/samples/v2beta1_wfs.yaml | 58 +- config/samples/v3_wfs.yaml | 41 +- config/samples/v3_wms.yaml | 69 +- config/webhook/manifests.yaml | 20 - go.mod | 180 +- go.sum | 377 ++-- hack/boilerplate.go.txt | 28 +- .../controller/blobdownload/blob_download.go | 225 --- .../blobdownload/blob_download_test.go | 332 --- .../controller/blobdownload/gpkg_download.sh | 175 -- .../capabilities_generator.go | 76 - .../capabilities_generator_test.go | 220 -- .../capabilitiesgenerator/default_bboxes.go | 132 -- .../capabilitiesgenerator/mapper.go | 691 ------- .../test_data/wfs_input.yaml | 78 - .../test_data/wms_input.yaml | 407 ---- internal/controller/configmaps.go | 147 -- internal/controller/configmaps_test.go | 31 - internal/controller/constants/constants.go | 29 - internal/controller/deployment.go | 296 --- .../featureinfo_generator.go | 49 - .../featureinfo_generator_test.go | 142 -- .../controller/featureinfogenerator/mapper.go | 69 - .../controller/horizontalpodautoscaler.go | 97 - internal/controller/ingressroute.go | 161 -- .../legendgenerator/legend-fixer.sh | 37 - .../legendgenerator/legend_generator.go | 85 - .../legendgenerator/legend_generator_test.go | 42 - internal/controller/legendgenerator/mapper.go | 140 -- .../test_data/expected/legend-fix.yaml | 121 -- .../test_data/expected/no-legend-fix.yaml | 14 - .../test_data/input/legend-fix.yaml | 527 ----- .../test_data/input/no-legend-fix.yaml | 193 -- .../mapfilegenerator/mapfile_generator.go | 84 - .../mapfile_generator_test.go | 110 - .../controller/mapfilegenerator/mapper.go | 325 --- .../test_data/expected/wfs.json | 73 - .../test_data/expected/wms_group.json | 468 ----- .../expected/wms_group_and_toplayer.json | 616 ------ .../test_data/expected/wms_groupless.json | 283 --- .../test_data/expected/wms_postgis.json | 93 - .../test_data/expected/wms_tif.json | 306 --- .../mapfilegenerator/test_data/input/wfs.yaml | 94 - .../test_data/input/wms_group.yaml | 576 ------ .../input/wms_group_and_toplayer.yaml | 783 -------- .../test_data/input/wms_groupless.yaml | 298 --- .../test_data/input/wms_postgis.yaml | 185 -- .../test_data/input/wms_tif.yaml | 438 ---- internal/controller/mapfilegenerator/types.go | 136 -- internal/controller/mapperutils/utils.go | 72 - internal/controller/mapserver/deployment.go | 200 -- .../controller/mapserver/deployment_test.go | 69 - .../test_data/expected_livenessprobe.yaml | 11 - .../test_data/expected_readinessprobe.yaml | 11 - .../test_data/expected_startupprobe.yaml | 11 - .../test_data/expected_volumemounts.yaml | 18 - .../mapserver/test_data/v2_input.yaml | 162 -- internal/controller/middleware.go | 48 - .../ogc_webservice_proxy.go | 93 - .../ogc_webservice_proxy_test.go | 46 - .../test_data/expected/named-toplayer.yaml | 12 - .../test_data/expected/unnamed-toplayer.yaml | 7 - .../test_data/input/named-toplayer.yaml | 52 - .../test_data/input/unnamed-toplayer.yaml | 51 - internal/controller/poddisruptionbudget.go | 42 - internal/controller/reconciler.go | 45 - internal/controller/service.go | 77 - internal/controller/shared_controller.go | 306 --- internal/controller/shared_controller_test.go | 456 ----- .../static/files/default_mapserver.conf | 8 - internal/controller/static/files/include.conf | 15 - internal/controller/static/files/ogc.lua | 83 - .../static/files/scraping-error.xml | 8 - internal/controller/static/reader.go | 26 - internal/controller/suite_test.go | 166 +- .../configmap-capabilities-generator.yaml | 118 -- .../expected/configmap-init-scripts.yaml | 190 -- .../expected/configmap-mapfile-generator.yaml | 122 -- .../expected/configmap-mapserver.yaml | 143 -- .../wfs/complete/expected/deployment.yaml | 270 --- .../expected/horizontalpodautoscaler.yaml | 52 - .../wfs/complete/expected/ingressroute.yaml | 43 - .../complete/expected/middleware-headers.yaml | 27 - .../expected/poddisruptionbudget.yaml | 31 - .../wfs/complete/expected/service.yaml | 41 - .../wfs/complete/input/ownerinfo.yaml | 23 - .../test_data/wfs/complete/input/wfs.yaml | 166 -- .../configmap-capabilities-generator.yaml | 22 - .../expected/configmap-init-scripts.yaml | 22 - .../expected/configmap-mapfile-generator.yaml | 22 - .../minimal/expected/configmap-mapserver.yaml | 26 - .../wfs/minimal/expected/deployment.yaml | 253 --- .../expected/horizontalpodautoscaler.yaml | 52 - .../wfs/minimal/expected/ingressroute.yaml | 35 - .../minimal/expected/middleware-headers.yaml | 26 - .../minimal/expected/poddisruptionbudget.yaml | 29 - .../wfs/minimal/expected/service.yaml | 40 - .../wfs/minimal/input/ownerinfo.yaml | 23 - .../test_data/wfs/minimal/input/wfs.yaml | 71 - .../configmap-capabilities-generator.yaml | 22 - .../expected/configmap-mapfile-generator.yaml | 22 - .../expected/configmap-mapserver.yaml | 26 - .../wfs/noprefetch/expected/deployment.yaml | 245 --- .../expected/horizontalpodautoscaler.yaml | 52 - .../wfs/noprefetch/expected/ingressroute.yaml | 35 - .../expected/middleware-headers.yaml | 26 - .../expected/poddisruptionbudget.yaml | 29 - .../wfs/noprefetch/expected/service.yaml | 40 - .../wfs/noprefetch/input/ownerinfo.yaml | 23 - .../test_data/wfs/noprefetch/input/wfs.yaml | 72 - .../configmap-capabilities-generator.yaml | 244 --- .../configmap-featureinfo-generator.yaml | 72 - .../expected/configmap-init-scripts.yaml | 190 -- .../expected/configmap-legend-generator.yaml | 86 - .../expected/configmap-mapfile-generator.yaml | 179 -- .../expected/configmap-mapserver.yaml | 147 -- .../configmap-ogc-webservice-proxy.yaml | 32 - .../wms/complete/expected/deployment.yaml | 404 ---- .../expected/horizontalpodautoscaler.yaml | 53 - .../wms/complete/expected/ingressroute.yaml | 62 - .../complete/expected/middleware-headers.yaml | 27 - .../expected/poddisruptionbudget.yaml | 31 - .../wms/complete/expected/service.yaml | 44 - .../wms/complete/input/ownerinfo.yaml | 36 - .../test_data/wms/complete/input/wms.yaml | 308 --- .../configmap-capabilities-generator.yaml | 150 -- .../configmap-featureinfo-generator.yaml | 52 - .../expected/configmap-init-scripts.yaml | 189 -- .../expected/configmap-legend-generator.yaml | 26 - .../expected/configmap-mapserver.yaml | 146 -- .../configmap-ogc-webservice-proxy.yaml | 31 - .../custom-mapfile/expected/deployment.yaml | 341 ---- .../expected/horizontalpodautoscaler.yaml | 52 - .../custom-mapfile/expected/ingressroute.yaml | 43 - .../expected/middleware-headers.yaml | 26 - .../expected/poddisruptionbudget.yaml | 29 - .../wms/custom-mapfile/expected/service.yaml | 42 - .../wms/custom-mapfile/input/ownerinfo.yaml | 36 - .../wms/custom-mapfile/input/wms.yaml | 126 -- .../configmap-capabilities-generator.yaml | 150 -- .../configmap-featureinfo-generator.yaml | 52 - .../expected/configmap-init-scripts.yaml | 189 -- .../expected/configmap-legend-generator.yaml | 26 - .../expected/configmap-mapfile-generator.yaml | 118 -- .../minimal/expected/configmap-mapserver.yaml | 146 -- .../configmap-ogc-webservice-proxy.yaml | 31 - .../wms/minimal/expected/deployment.yaml | 364 ---- .../expected/horizontalpodautoscaler.yaml | 52 - .../wms/minimal/expected/ingressroute.yaml | 43 - .../minimal/expected/middleware-headers.yaml | 26 - .../minimal/expected/poddisruptionbudget.yaml | 29 - .../wms/minimal/expected/service.yaml | 42 - .../wms/minimal/input/ownerinfo.yaml | 36 - .../test_data/wms/minimal/input/wms.yaml | 131 -- .../configmap-capabilities-generator.yaml | 150 -- .../configmap-featureinfo-generator.yaml | 52 - .../expected/configmap-legend-generator.yaml | 26 - .../expected/configmap-mapfile-generator.yaml | 118 -- .../expected/configmap-mapserver.yaml | 146 -- .../configmap-ogc-webservice-proxy.yaml | 31 - .../wms/noprefetch/expected/deployment.yaml | 356 ---- .../expected/horizontalpodautoscaler.yaml | 52 - .../wms/noprefetch/expected/ingressroute.yaml | 43 - .../expected/middleware-headers.yaml | 26 - .../expected/poddisruptionbudget.yaml | 29 - .../wms/noprefetch/expected/service.yaml | 42 - .../wms/noprefetch/input/ownerinfo.yaml | 36 - .../test_data/wms/noprefetch/input/wms.yaml | 132 -- .../configmap-capabilities-generator.yaml | 150 -- .../configmap-featureinfo-generator.yaml | 52 - .../expected/configmap-init-scripts.yaml | 189 -- .../expected/configmap-legend-generator.yaml | 26 - .../expected/configmap-mapfile-generator.yaml | 118 -- .../patches/expected/configmap-mapserver.yaml | 146 -- .../configmap-ogc-webservice-proxy.yaml | 31 - .../wms/patches/expected/deployment.yaml | 372 ---- .../expected/horizontalpodautoscaler.yaml | 49 - .../wms/patches/expected/ingressroute.yaml | 43 - .../patches/expected/middleware-headers.yaml | 26 - .../patches/expected/poddisruptionbudget.yaml | 29 - .../wms/patches/expected/service.yaml | 42 - .../wms/patches/input/ownerinfo.yaml | 36 - .../test_data/wms/patches/input/wms.yaml | 255 --- internal/controller/types/types.go | 21 - internal/controller/utils/utils.go | 66 - internal/controller/wfs_controller.go | 118 +- internal/controller/wfs_controller_test.go | 340 +--- internal/controller/wms_controller.go | 195 +- internal/controller/wms_controller_test.go | 298 +-- internal/webhook/v3/shared_webhook.go | 114 -- internal/webhook/v3/test_data/ownerinfo.yaml | 39 - internal/webhook/v3/test_data/v3_wfs.yaml | 74 - internal/webhook/v3/test_data/v3_wms.yaml | 75 - internal/webhook/v3/webhook_suite_test.go | 122 +- internal/webhook/v3/wfs_webhook.go | 70 +- internal/webhook/v3/wfs_webhook_test.go | 480 +---- internal/webhook/v3/wms_webhook.go | 96 +- internal/webhook/v3/wms_webhook_test.go | 676 +------ test/e2e/e2e_suite_test.go | 36 +- test/e2e/e2e_test.go | 223 ++- test/utils/utils.go | 7 +- 247 files changed, 1619 insertions(+), 33933 deletions(-) delete mode 100644 .github/workflows/build-and-publish-image.yml delete mode 100644 DEVELOPMENT.md delete mode 100644 LICENSE delete mode 100644 api/v2beta1/shared_conversion.go delete mode 100644 api/v2beta1/shared_types.go delete mode 100644 api/v2beta1/wms_conversion_test.go delete mode 100644 api/v3/shared_types.go delete mode 100644 api/v3/shared_validation.go delete mode 100644 api/v3/wfs_validation.go delete mode 100644 api/v3/wms_types_test.go delete mode 100644 api/v3/wms_validation.go delete mode 100755 build-push-deploy-locally.sh delete mode 100644 config/crd/bases/embed.go delete mode 100644 config/crd/bases/pdok.nl_wfs.yaml create mode 100644 config/crd/bases/pdok.nl_wfses.yaml delete mode 100644 config/crd/bases/pdok.nl_wms.yaml create mode 100644 config/crd/bases/pdok.nl_wmses.yaml delete mode 100644 config/crd/update_openapi.go delete mode 100644 config/samples/samples.go delete mode 100644 internal/controller/blobdownload/blob_download.go delete mode 100644 internal/controller/blobdownload/blob_download_test.go delete mode 100644 internal/controller/blobdownload/gpkg_download.sh delete mode 100644 internal/controller/capabilitiesgenerator/capabilities_generator.go delete mode 100644 internal/controller/capabilitiesgenerator/capabilities_generator_test.go delete mode 100644 internal/controller/capabilitiesgenerator/default_bboxes.go delete mode 100644 internal/controller/capabilitiesgenerator/mapper.go delete mode 100644 internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml delete mode 100644 internal/controller/capabilitiesgenerator/test_data/wms_input.yaml delete mode 100644 internal/controller/configmaps.go delete mode 100644 internal/controller/configmaps_test.go delete mode 100644 internal/controller/constants/constants.go delete mode 100644 internal/controller/deployment.go delete mode 100644 internal/controller/featureinfogenerator/featureinfo_generator.go delete mode 100644 internal/controller/featureinfogenerator/featureinfo_generator_test.go delete mode 100644 internal/controller/featureinfogenerator/mapper.go delete mode 100644 internal/controller/horizontalpodautoscaler.go delete mode 100644 internal/controller/ingressroute.go delete mode 100755 internal/controller/legendgenerator/legend-fixer.sh delete mode 100644 internal/controller/legendgenerator/legend_generator.go delete mode 100644 internal/controller/legendgenerator/legend_generator_test.go delete mode 100644 internal/controller/legendgenerator/mapper.go delete mode 100644 internal/controller/legendgenerator/test_data/expected/legend-fix.yaml delete mode 100644 internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml delete mode 100644 internal/controller/legendgenerator/test_data/input/legend-fix.yaml delete mode 100644 internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml delete mode 100644 internal/controller/mapfilegenerator/mapfile_generator.go delete mode 100644 internal/controller/mapfilegenerator/mapfile_generator_test.go delete mode 100644 internal/controller/mapfilegenerator/mapper.go delete mode 100644 internal/controller/mapfilegenerator/test_data/expected/wfs.json delete mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_group.json delete mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json delete mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json delete mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json delete mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_tif.json delete mode 100644 internal/controller/mapfilegenerator/test_data/input/wfs.yaml delete mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_group.yaml delete mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml delete mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml delete mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml delete mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml delete mode 100644 internal/controller/mapfilegenerator/types.go delete mode 100644 internal/controller/mapperutils/utils.go delete mode 100644 internal/controller/mapserver/deployment.go delete mode 100644 internal/controller/mapserver/deployment_test.go delete mode 100644 internal/controller/mapserver/test_data/expected_livenessprobe.yaml delete mode 100644 internal/controller/mapserver/test_data/expected_readinessprobe.yaml delete mode 100644 internal/controller/mapserver/test_data/expected_startupprobe.yaml delete mode 100644 internal/controller/mapserver/test_data/expected_volumemounts.yaml delete mode 100644 internal/controller/mapserver/test_data/v2_input.yaml delete mode 100644 internal/controller/middleware.go delete mode 100644 internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go delete mode 100644 internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go delete mode 100644 internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml delete mode 100644 internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml delete mode 100644 internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml delete mode 100644 internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml delete mode 100644 internal/controller/poddisruptionbudget.go delete mode 100644 internal/controller/reconciler.go delete mode 100644 internal/controller/service.go delete mode 100644 internal/controller/shared_controller.go delete mode 100644 internal/controller/shared_controller_test.go delete mode 100644 internal/controller/static/files/default_mapserver.conf delete mode 100644 internal/controller/static/files/include.conf delete mode 100644 internal/controller/static/files/ogc.lua delete mode 100644 internal/controller/static/files/scraping-error.xml delete mode 100644 internal/controller/static/reader.go delete mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wfs/complete/expected/service.yaml delete mode 100644 internal/controller/test_data/wfs/complete/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wfs/complete/input/wfs.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/expected/service.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wfs/minimal/input/wfs.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/expected/service.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wfs/noprefetch/input/wfs.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wms/complete/expected/service.yaml delete mode 100644 internal/controller/test_data/wms/complete/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wms/complete/input/wms.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/service.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wms/custom-mapfile/input/wms.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wms/minimal/expected/service.yaml delete mode 100644 internal/controller/test_data/wms/minimal/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wms/minimal/input/wms.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/expected/service.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wms/noprefetch/input/wms.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/deployment.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/ingressroute.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/middleware-headers.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml delete mode 100644 internal/controller/test_data/wms/patches/expected/service.yaml delete mode 100644 internal/controller/test_data/wms/patches/input/ownerinfo.yaml delete mode 100644 internal/controller/test_data/wms/patches/input/wms.yaml delete mode 100644 internal/controller/types/types.go delete mode 100644 internal/controller/utils/utils.go delete mode 100644 internal/webhook/v3/shared_webhook.go delete mode 100644 internal/webhook/v3/test_data/ownerinfo.yaml delete mode 100644 internal/webhook/v3/test_data/v3_wfs.yaml delete mode 100644 internal/webhook/v3/test_data/v3_wms.yaml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4dafb3c..0e0eed2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Kubebuilder DevContainer", - "image": "docker.io/golang:1.24", + "image": "docker.io/golang:1.23", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {}, "ghcr.io/devcontainers/features/git:1": {} diff --git a/.github/workflows/build-and-publish-image.yml b/.github/workflows/build-and-publish-image.yml deleted file mode 100644 index 4f09751..0000000 --- a/.github/workflows/build-and-publish-image.yml +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: Build -env: - image: pdok/mapserver-operator -on: - push: - tags: - - '*' -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Docker meta - id: docker_meta - uses: docker/metadata-action@v3 - with: - images: ${{ env.image }} - tags: | - type=semver,pattern={{major}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{version}} - - name: Login to PDOK Docker Hub - if: startsWith(env.image, 'pdok/') - uses: docker/login-action@v1 - with: - username: koalapdok - password: ${{ secrets.DOCKERHUB_PUSH }} - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - name: Build and push - id: docker_build - uses: docker/build-push-action@v2 - with: - push: true - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new - - # Temp fix to cleanup cache - # https://github.com/docker/build-push-action/issues/252 - # https://github.com/moby/buildkit/issues/1896 - name: Move cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache - - name: Build result notification - if: success() || failure() - uses: 8398a7/action-slack@v3 - with: - fields: all - status: custom - custom_payload: | - { - attachments: [{ - color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', - text: `${process.env.AS_WORKFLOW} ${{ job.status }} for ${process.env.AS_REPO}!\n${process.env.AS_JOB} job on ${process.env.AS_REF} (commit: ${process.env.AS_COMMIT}, version: ${{ steps.docker_meta.outputs.version }}) by ${process.env.AS_AUTHOR} took ${process.env.AS_TOOK}`, - }] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0c9f2db..4951e33 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,13 +2,11 @@ name: Lint on: push: - branches: - - master pull_request: jobs: lint: - name: Linting on Ubuntu + name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code @@ -22,4 +20,4 @@ jobs: - name: Run linter uses: golangci/golangci-lint-action@v6 with: - version: v1.64.8 + version: v1.63.4 diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 0ce9473..b2eda8c 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -2,13 +2,11 @@ name: E2E Tests on: push: - branches: - - master pull_request: jobs: test-e2e: - name: End-2-End on Ubuntu + name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1117ec9..fc2e80d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,13 +2,11 @@ name: Tests on: push: - branches: - - master pull_request: jobs: test: - name: Testing on Ubuntu + name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code diff --git a/.golangci.yml b/.golangci.yml index 331b6f2..6b29746 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,116 +1,47 @@ ---- run: - # Timeout for analysis. timeout: 5m - - # Modules download mode (do not modify go.mod) - modules-download-mode: readonly - - # Include test files (see below to exclude certain linters) - tests: true + allow-parallel-runners: true issues: + # don't skip warning about doc comments + # don't exclude the default set of lint + exclude-use-default: false + # restore some of the defaults + # (fill in the rest as needed) exclude-rules: - # Exclude certain linters for test code - - path: "_test\\.go" + - path: "api/*" + linters: + - lll + - path: "internal/*" linters: - - bodyclose - dupl - - dogsled - - funlen - - gosec - -output: - formats: - - format: colored-line-number - path: stdout - print-issued-lines: true - print-linter-name: true - -linters-settings: - depguard: - rules: - main: - # Packages that are not allowed where the value is a suggestion. - deny: - - pkg: "github.com/pkg/errors" - desc: Should be replaced by standard lib errors package - cyclop: - # The maximal code complexity to report. - max-complexity: 15 - skip-tests: true - funlen: - lines: 100 - nestif: - min-complexity: 6 - forbidigo: - forbid: - - http\.NotFound.* # return RFC 7807 problem details instead - - http\.Error.* # return RFC 7807 problem details instead - gomoddirectives: - replace-allow-list: - - github.com/abbot/go-http-auth # https://github.com/traefik/traefik/issues/6873#issuecomment-637654361 - + - lll linters: disable-all: true enable: - # enabled by default by golangci-lint - - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases - - gosimple # specializes in simplifying a code - - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - - ineffassign # detects when assignments to existing variables are not used - - staticcheck # is a go vet on steroids, applying a ton of static analysis checks - - typecheck # like the front-end of a Go compiler, parses and type-checks Go code - - unused # checks for unused constants, variables, functions and types - # extra enabled by us - - asasalint # checks for pass []any as any in variadic func(...any) - - asciicheck # checks that your code does not contain non-ASCII identifiers - - bidichk # checks for dangerous unicode character sequences - - bodyclose # checks whether HTTP response body is closed successfully - - cyclop # checks function and package cyclomatic complexity - - dupl # tool for code clone detection - - durationcheck # checks for two durations multiplied together - - dogsled # find assignments/declarations with too many blank identifiers - - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error - - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 - - exhaustive # checks exhaustiveness of enum switch statements - - exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions - - copyloopvar # checks for pointers to enclosing loop variables - - fatcontext # detects nested contexts in loops and function literals - - forbidigo # forbids identifiers - - funlen # tool for detection of long functions - - gocheckcompilerdirectives # validates go compiler directive comments (//go:) - - goconst # finds repeated strings that could be replaced by a constant - - gocritic # provides diagnostics that check for bugs, performance and style issues - - gofmt # checks if the code is formatted according to 'gofmt' command - - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt - - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod - - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations - - goprintffuncname # checks that printf-like functions are named with f at the end - - gosec # inspects source code for security problems - - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) - - makezero # finds slice declarations with non-zero initial length - - mirror # reports wrong mirror patterns of bytes/strings usage - - misspell # finds commonly misspelled English words - - nakedret # finds naked returns in functions greater than a specified function length - - nestif # reports deeply nested if statements - - nilerr # finds the code that returns nil even if it checks that the error is not nil - - nolintlint # reports ill-formed or insufficient nolint directives - - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL - - perfsprint # Golang linter for performance, aiming at usages of fmt.Sprintf which have faster alternatives - - predeclared # finds code that shadows one of Go's predeclared identifiers - - promlinter # checks Prometheus metrics naming via promlint - - reassign # checks that package variables are not reassigned - - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint - - rowserrcheck # checks whether Err of rows is checked successfully - - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed - - sloglint # A Go linter that ensures consistent code style when using log/slog - - tagliatelle # checks the struct tags. - - testableexamples # checks if examples are testable (have an expected output) - - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes - - usetesting # detects using os.Setenv instead of t.Setenv since Go1.17 - - unconvert # removes unnecessary type conversions - - unparam # reports unused function parameters - - usestdlibvars # detects the possibility to use variables/constants from the Go standard library - - wastedassign # finds wasted assignment statements - fast: false \ No newline at end of file + - dupl + - errcheck + - copyloopvar + - ginkgolinter + - goconst + - gocyclo + - gofmt + - goimports + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - prealloc + - revive + - staticcheck + - typecheck + - unconvert + - unparam + - unused + +linters-settings: + revive: + rules: + - name: comment-spacings diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index ff22131..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,5 +0,0 @@ -## Local testing - -- Start an empty cluster using `k8s-clusters/local-test/empty-cluster.sh` -- Build and push the controller to the cluster using `build-and-push-locally.sh ` -- Deploy a service to the cluster, for example (running from `k8s-clusters/local-test`): `OWNER=kadaster TECHNICAL_NAME=ad docker-compose -f ./docker-compose.yaml -f ./bundle-pollers/docker-compose.services.yaml up kustomize-init` diff --git a/Dockerfile b/Dockerfile index 735a9dc..348b837 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,12 @@ # Build the manager binary -FROM docker.io/golang:1.24 AS builder +FROM docker.io/golang:1.23 AS builder ARG TARGETOS ARG TARGETARCH -#COPY --from=repos ./smooth-operator /smooth-operator -#COPY --from=repos ./ogc-specifications /ogc-specifications - WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum - # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1e6423f..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024-2025 Publieke Dienstverlening op de Kaart - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index bdc20dd..9a4ab3b 100644 --- a/Makefile +++ b/Makefile @@ -43,9 +43,7 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) rbac:roleName=manager-role crd:allowDangerousTypes=true webhook paths="./..." output:crd:artifacts:config=config/crd/bases - go run config/crd/update_openapi.go config/crd/bases -## allowDangerousTypes=true for v2beta structs + $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. @@ -65,8 +63,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests. # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. -# Prometheus and CertManager are installed by default; skip with: -# - PROMETHEUS_INSTALL_SKIP=true +# CertManager is installed by default; skip with: # - CERT_MANAGER_INSTALL_SKIP=true .PHONY: test-e2e test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. @@ -175,12 +172,12 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions KUSTOMIZE_VERSION ?= v5.5.0 -CONTROLLER_TOOLS_VERSION ?= v0.17.1 +CONTROLLER_TOOLS_VERSION ?= v0.17.2 #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v1.64.8 +GOLANGCI_LINT_VERSION ?= v1.63.4 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. diff --git a/README.md b/README.md index afabaa5..e75fcab 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,13 @@ # mapserver-operator -_Kubernetes controller/operator to serve WFS and WMS instances._ - -[![Build](https://github.com/PDOK/mapserver-operator/actions/workflows/build-and-publish-image.yml/badge.svg)](https://github.com/PDOK/mapserver-operator/actions/workflows/build-and-publish-image.yml) -[![Lint (go)](https://github.com/PDOK/mapserver-operator/actions/workflows/lint.yml/badge.svg)](https://github.com/PDOK/mapserver-operator/actions/workflows/lint.yml) -[![GitHub license](https://img.shields.io/github/license/PDOK/mapserver-operator)](https://github.com/PDOK/mapserver-operator/blob/master/LICENSE) +// TODO(user): Add simple overview of use/purpose ## Description -This Kubernetes controller cq operator (an operator could be described as a specialized controller) -ensures that the necessary resources are created or kept up-to-date in a cluster -to deploy instances of the [Web Map Service](https://www.ogc.org/standards/wms/)(WMS) and [Web Features Service](https://www.ogc.org/standards/wfs/)(WFS). This repository is a complete solution to deploy WMS and WFS services according to CR schemas. -This operator uses two Custom Resources(CR) called _WMS_ and _WFS_ as the input for the deployment, which is also defined in this repository. +// TODO(user): An in-depth paragraph about your project and overview of use ## Getting Started ### Prerequisites -- go version v1.24.0+ +- go version v1.23.0+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. @@ -73,54 +66,70 @@ make uninstall make undeploy ``` -## Develop +## Project Distribution -The project is written in Go and scaffolded with [kubebuilder](https://kubebuilder.io). +Following the options to release and provide this solution to the users. -### kubebuilder +### By providing a bundle with all YAML files -Read the manual when you want/need to make changes. -E.g. run `make test` before committing. +1. Build the installer for the image built and published in the registry: -### Linting +```sh +make build-installer IMG=/mapserver-operator:tag +``` -Install [golangci-lint](https://golangci-lint.run/usage/install/) and run `golangci-lint run` -from the root. -(Don't run `make lint`, it uses an old version of golangci-lint.) +**NOTE:** The makefile target mentioned above generates an 'install.yaml' +file in the dist directory. This file contains all the resources built +with Kustomize, which are necessary to install this project without its +dependencies. -# Contributing +2. Using the installer -### How to contribute -Mapserver-operator is solely developed by PDOK. Contributions are however always welcome. If you have any questions or suggestions you can create an issue in the issue tracker. +Users can just run 'kubectl apply -f ' to install +the project, i.e.: -### Contact -The maintainers can be contacted through the issue tracker. +```sh +kubectl apply -f https://raw.githubusercontent.com//mapserver-operator//dist/install.yaml +``` -# Authors -This project is developed by [PDOK](https://www.pdok.nl/), a platform for publication of geographic datasets of Dutch governmental institutions. +### By providing a Helm Chart -# License +1. Build the chart using the optional helm plugin +```sh +kubebuilder edit --plugins=helm/v1-alpha ``` -MIT License - -Copyright (c) 2024-2025 Publieke Dienstverlening op de Kaart - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` + +2. See that a chart was generated under 'dist/chart', and users +can obtain this solution from there. + +**NOTE:** If you change the project, you need to update the Helm Chart +using the same command above to sync the latest changes. Furthermore, +if you create webhooks, you need to use the above command with +the '--force' flag and manually ensure that any custom configuration +previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' +is manually re-applied afterwards. + +## Contributing +// TODO(user): Add detailed information on how you would like others to contribute to this project + +**NOTE:** Run `make help` for more information on all potential `make` targets + +More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) + +## License + +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/api/v2beta1/groupversion_info.go b/api/v2beta1/groupversion_info.go index 7033d98..0c3c988 100644 --- a/api/v2beta1/groupversion_info.go +++ b/api/v2beta1/groupversion_info.go @@ -1,25 +1,17 @@ /* -MIT License - -Copyright (c) 2024 Publieke Dienstverlening op de Kaart - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ // Package v2beta1 contains API Schema definitions for the v2beta1 API group. diff --git a/api/v2beta1/shared_conversion.go b/api/v2beta1/shared_conversion.go deleted file mode 100644 index f497eb6..0000000 --- a/api/v2beta1/shared_conversion.go +++ /dev/null @@ -1,289 +0,0 @@ -package v2beta1 - -import ( - "net/url" - "strings" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/constants" - smoothoperatormodel "github.com/pdok/smooth-operator/model" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - - autoscalingv2 "k8s.io/api/autoscaling/v2" - corev1 "k8s.io/api/core/v1" -) - -func fixUnicode(val string) string { - return strings.ReplaceAll(val, "\\xF6", "รถ") -} - -func ConvertOptionsV2ToV3(src *WMSWFSOptions) *pdoknlv3.Options { - defaults := pdoknlv3.GetDefaultOptions() - - if src == nil { - return defaults - } - - return &pdoknlv3.Options{ - BaseOptions: pdoknlv3.BaseOptions{ - AutomaticCasing: src.AutomaticCasing, - IncludeIngress: src.IncludeIngress, - PrefetchData: smoothoperatorutils.PointerVal(src.PrefetchData, defaults.PrefetchData), - }, - WMSOptions: pdoknlv3.WMSOptions{ - ValidateRequests: smoothoperatorutils.PointerVal(src.ValidateRequests, defaults.ValidateRequests), - RewriteGroupToDataLayers: smoothoperatorutils.PointerVal(src.RewriteGroupToDataLayers, defaults.RewriteGroupToDataLayers), - DisableWebserviceProxy: smoothoperatorutils.PointerVal(src.DisableWebserviceProxy, defaults.DisableWebserviceProxy), - ValidateChildStyleNameEqual: smoothoperatorutils.PointerVal(src.ValidateChildStyleNameEqual, defaults.ValidateChildStyleNameEqual), - }, - } -} - -func ConvertOptionsV3ToV2(src *pdoknlv3.Options) *WMSWFSOptions { - if src == nil { - src = pdoknlv3.GetDefaultOptions() - } - - return &WMSWFSOptions{ - AutomaticCasing: src.AutomaticCasing, - IncludeIngress: src.IncludeIngress, - PrefetchData: &src.PrefetchData, - ValidateRequests: &src.ValidateRequests, - RewriteGroupToDataLayers: &src.RewriteGroupToDataLayers, - DisableWebserviceProxy: &src.DisableWebserviceProxy, - ValidateChildStyleNameEqual: &src.ValidateChildStyleNameEqual, - } -} - -//nolint:gosec -func ConvertAutoscaling(src Autoscaling) *pdoknlv3.HorizontalPodAutoscalerPatch { - hpa := &pdoknlv3.HorizontalPodAutoscalerPatch{} - - if src.MinReplicas != nil { - //nolint:gosec - hpa.MinReplicas = smoothoperatorutils.Pointer(int32(*src.MinReplicas)) - } - - if src.MaxReplicas != nil { - //nolint:gosec - hpa.MaxReplicas = smoothoperatorutils.Pointer(int32(*src.MaxReplicas)) - } - - metrics := make([]autoscalingv2.MetricSpec, 0) - if src.AverageCPUUtilization != nil { - metrics = append(metrics, autoscalingv2.MetricSpec{ - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: corev1.ResourceCPU, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: smoothoperatorutils.Pointer(int32(*src.AverageCPUUtilization)), - }, - }, - }) - hpa.Metrics = metrics - } - - return hpa -} - -func ConvertResources(src corev1.ResourceRequirements) corev1.PodSpec { - targetResources := src - - if src.Requests != nil { - if val, ok := src.Requests["ephemeralStorage"]; ok { - targetResources.Requests[corev1.ResourceEphemeralStorage] = val - delete(targetResources.Requests, "ephemeralStorage") - } - } - if src.Limits != nil { - if val, ok := src.Limits["ephemeralStorage"]; ok { - targetResources.Limits[corev1.ResourceEphemeralStorage] = val - delete(targetResources.Limits, "ephemeralStorage") - } - } - - return corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: constants.MapserverName, - Resources: targetResources, - }, - }, - } -} - -func ConvertColumnAndAliasesV2ToColumnsWithAliasV3(columns []string, aliases map[string]string) []pdoknlv3.Column { - v3Columns := make([]pdoknlv3.Column, 0) - for _, column := range columns { - col := pdoknlv3.Column{ - Name: column, - } - - // TODO - multiple aliases per column possible? - if alias, ok := aliases[column]; ok { - col.Alias = &alias - } - - v3Columns = append(v3Columns, col) - } - - return v3Columns -} - -func ConvertColumnsWithAliasV3ToColumnsAndAliasesV2(columns []pdoknlv3.Column) ([]string, map[string]string) { - v2Columns := make([]string, 0) - v2Aliases := make(map[string]string) - - for _, col := range columns { - v2Columns = append(v2Columns, col.Name) - - if col.Alias != nil { - v2Aliases[col.Name] = *col.Alias - } - } - - return v2Columns, v2Aliases -} - -func ConvertV2DataToV3(v2 Data) pdoknlv3.Data { - v3 := pdoknlv3.Data{} - - if v2.GPKG != nil { - v3.Gpkg = &pdoknlv3.Gpkg{ - BlobKey: v2.GPKG.BlobKey, - TableName: v2.GPKG.Table, - GeometryType: v2.GPKG.GeometryType, - Columns: ConvertColumnAndAliasesV2ToColumnsWithAliasV3( - v2.GPKG.Columns, - v2.GPKG.Aliases, - ), - } - } - - if v2.Postgis != nil { - v3.Postgis = &pdoknlv3.Postgis{ - TableName: v2.Postgis.Table, - GeometryType: v2.Postgis.GeometryType, - Columns: ConvertColumnAndAliasesV2ToColumnsWithAliasV3( - v2.Postgis.Columns, - v2.Postgis.Aliases, - ), - } - } - - if v2.Tif != nil { - v3.TIF = &pdoknlv3.TIF{ - BlobKey: v2.Tif.BlobKey, - Resample: smoothoperatorutils.PointerVal(v2.Tif.Resample, "NEAREST"), - Offsite: v2.Tif.Offsite, - GetFeatureInfoIncludesClass: smoothoperatorutils.PointerVal(v2.Tif.GetFeatureInfoIncludesClass, false), - } - } - - return v3 -} - -func ConvertV3DataToV2(v3 pdoknlv3.Data) Data { - v2 := Data{} - - if v3.Gpkg != nil { - columns, aliases := ConvertColumnsWithAliasV3ToColumnsAndAliasesV2(v3.Gpkg.Columns) - v2.GPKG = &GPKG{ - BlobKey: v3.Gpkg.BlobKey, - Table: v3.Gpkg.TableName, - GeometryType: v3.Gpkg.GeometryType, - Columns: columns, - Aliases: aliases, - } - } - - if v3.Postgis != nil { - columns, aliases := ConvertColumnsWithAliasV3ToColumnsAndAliasesV2(v3.Postgis.Columns) - v2.Postgis = &Postgis{ - Table: v3.Postgis.TableName, - GeometryType: v3.Postgis.GeometryType, - Columns: columns, - Aliases: aliases, - } - } - - if v3.TIF != nil { - v2.Tif = &Tif{ - BlobKey: v3.TIF.BlobKey, - Offsite: v3.TIF.Offsite, - Resample: &v3.TIF.Resample, - GetFeatureInfoIncludesClass: &v3.TIF.GetFeatureInfoIncludesClass, - } - } - - return v2 -} - -func NewV2KubernetesObject(lifecycle *smoothoperatormodel.Lifecycle, podSpecPatch corev1.PodSpec, scalingSpec *pdoknlv3.HorizontalPodAutoscalerPatch) Kubernetes { - kub := Kubernetes{} - - if lifecycle != nil && lifecycle.TTLInDays != nil { - kub.Lifecycle = &Lifecycle{ - TTLInDays: smoothoperatorutils.Pointer(int(*lifecycle.TTLInDays)), - } - } - - kub.Resources = &podSpecPatch.Containers[0].Resources - - if scalingSpec != nil { - kub.Autoscaling = &Autoscaling{} - - if scalingSpec.MaxReplicas != nil { - kub.Autoscaling.MaxReplicas = smoothoperatorutils.Pointer(int(*scalingSpec.MaxReplicas)) - } - - if scalingSpec.MinReplicas != nil { - kub.Autoscaling.MinReplicas = smoothoperatorutils.Pointer(int(*scalingSpec.MinReplicas)) - } - - if scalingSpec.Metrics != nil { - kub.Autoscaling.AverageCPUUtilization = smoothoperatorutils.Pointer( - int(*scalingSpec.Metrics[0].Resource.Target.AverageUtilization), - ) - } - } - - return kub -} - -func LabelsToV2General(labels map[string]string) General { - general := General{ - Dataset: labels["dataset"], - DatasetOwner: labels["dataset-owner"], - DataVersion: nil, - } - - if serviceVersion, ok := labels["service-version"]; ok { - general.ServiceVersion = &serviceVersion - } - - if theme, ok := labels["theme"]; ok { - general.Theme = &theme - } - - return general -} - -func CreateBaseURL(host string, kind string, general General) (*smoothoperatormodel.URL, error) { - baseURL, err := url.Parse(host + "/") - if err != nil { - return nil, err - } - baseURL = baseURL.JoinPath(general.DatasetOwner, general.Dataset) - if general.Theme != nil { - baseURL = baseURL.JoinPath(*general.Theme) - } - baseURL = baseURL.JoinPath(kind) - - if general.ServiceVersion != nil { - baseURL = baseURL.JoinPath(*general.ServiceVersion) - } - - return &smoothoperatormodel.URL{URL: baseURL}, nil -} diff --git a/api/v2beta1/shared_types.go b/api/v2beta1/shared_types.go deleted file mode 100644 index 9ea921f..0000000 --- a/api/v2beta1/shared_types.go +++ /dev/null @@ -1,145 +0,0 @@ -package v2beta1 - -import ( - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Status - The status for custom resources managed by the operator-sdk. -type Status struct { - Conditions []Condition `json:"conditions,omitempty"` - Deployment *string `json:"deployment,omitempty"` - Resources []Resources `json:"resources,omitempty"` -} - -// Condition - the condition for the ansible operator -// https://github.com/operator-framework/operator-sdk/blob/master/internal/ansible/controller/status/types.go#L101 -type Condition struct { - Type ConditionType `json:"type"` - Status ConditionStatus `json:"status"` - LastTransitionTime metav1.Time `json:"lastTransitionTime"` - AnsibleResult *ResultAnsible `json:"ansibleResult,omitempty"` - Reason string `json:"reason"` - Message string `json:"message"` -} - -// ConditionType specifies a string for field ConditionType -type ConditionType string - -// ConditionStatus specifies a string for field ConditionType -type ConditionStatus string - -// This const specifies allowed fields for Status -const ( - ConditionTrue ConditionStatus = "True" - ConditionFalse ConditionStatus = "False" - ConditionUnknown ConditionStatus = "Unknown" -) - -// ResultAnsible - encapsulation of the ansible result. 'AnsibleResult' is turned around in struct to comply with linting -type ResultAnsible struct { - Ok int `json:"ok"` - Changed int `json:"changed"` - Skipped int `json:"skipped"` - Failures int `json:"failures"` - TimeOfCompletion string `json:"completion"` -} - -// Resources is the struct for the resources field within status -type Resources struct { - APIVersion *string `json:"apiversion,omitempty"` - Kind *string `json:"kind,omitempty"` - Name *string `json:"name,omitempty"` -} - -// General is the struct with all generic fields for the crds -type General struct { - Dataset string `json:"dataset"` - Theme *string `json:"theme,omitempty"` - DatasetOwner string `json:"datasetOwner"` - ServiceVersion *string `json:"serviceVersion,omitempty"` - DataVersion *string `json:"dataVersion,omitempty"` -} - -// Kubernetes is the struct with all fields that can be defined in kubernetes fields in the crds -type Kubernetes struct { - Autoscaling *Autoscaling `json:"autoscaling,omitempty"` - HealthCheck *HealthCheck `json:"healthCheck,omitempty"` - Resources *corev1.ResourceRequirements `json:"resources,omitempty"` - Lifecycle *Lifecycle `json:"lifecycle,omitempty"` -} - -// Autoscaling is the struct with all fields to configure autoscalers for the crs -type Autoscaling struct { - AverageCPUUtilization *int `json:"averageCpuUtilization,omitempty"` - MinReplicas *int `json:"minReplicas,omitempty"` - MaxReplicas *int `json:"maxReplicas,omitempty"` -} - -// HealthCheck is the struct with all fields to configure healthchecks for the crs -type HealthCheck struct { - Querystring *string `json:"querystring,omitempty"` - Mimetype *string `json:"mimetype,omitempty"` - Boundingbox *string `json:"boundingbox,omitempty"` -} - -// Lifecycle is the struct with the fields to configure lifecycle settings for the resources -type Lifecycle struct { - TTLInDays *int `json:"ttlInDays,omitempty"` -} - -// WMSWFSOptions is the struct with options available in the operator -type WMSWFSOptions struct { - // +kubebuilder:default:=true - IncludeIngress bool `json:"includeIngress"` - // +kubebuilder:default:=true - AutomaticCasing bool `json:"automaticCasing"` - // +kubebuilder:default:=true - ValidateRequests *bool `json:"validateRequests,omitempty"` - RewriteGroupToDataLayers *bool `json:"rewriteGroupToDataLayers,omitempty"` - DisableWebserviceProxy *bool `json:"disableWebserviceProxy,omitempty"` - // +kubebuilder:default:=true - PrefetchData *bool `json:"prefetchData,omitempty"` - ValidateChildStyleNameEqual *bool `json:"validateChildStyleNameEqual,omitempty"` -} - -// Authority is a struct for the authority fields in WMS and WFS crds -type Authority struct { - Name string `json:"name"` - URL string `json:"url"` -} - -// Data is a struct for the data field for a WMSLayer or WFS FeatureType -type Data struct { - GPKG *GPKG `json:"gpkg,omitempty"` - Postgis *Postgis `json:"postgis,omitempty"` - Tif *Tif `json:"tif,omitempty"` -} - -// GPKG is a struct for the gpkg field for a WMSLayer or WFS FeatureType -type GPKG struct { - BlobKey string `json:"blobKey"` - Table string `json:"table"` - GeometryType string `json:"geometryType"` - Columns []string `json:"columns"` - // In a new version Aliases should become part of Columns - Aliases map[string]string `json:"aliases,omitempty"` -} - -// Postgis is a struct for the Postgis db config for a WMSLayer or WFS FeatureType -// connection details are passed through the environment -type Postgis struct { - Table string `json:"table"` - GeometryType string `json:"geometryType"` - Columns []string `json:"columns"` - // In a new version Aliases should become part of Columns - Aliases map[string]string `json:"aliases,omitempty"` -} - -// Tif is a struct for the Tif field for a WMSLayer -type Tif struct { - BlobKey string `json:"blobKey"` - GetFeatureInfoIncludesClass *bool `json:"getFeatureInfoIncludesClass,omitempty"` - Offsite *string `json:"offsite,omitempty"` - Resample *string `json:"resample,omitempty"` -} diff --git a/api/v2beta1/wfs_conversion.go b/api/v2beta1/wfs_conversion.go index 24664fc..54a3687 100644 --- a/api/v2beta1/wfs_conversion.go +++ b/api/v2beta1/wfs_conversion.go @@ -1,38 +1,27 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v2beta1 import ( "log" - "strconv" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatormodel "github.com/pdok/smooth-operator/model" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" "sigs.k8s.io/controller-runtime/pkg/conversion" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) // ConvertTo converts this WFS (v2beta1) to the Hub version (v3). @@ -41,246 +30,16 @@ func (src *WFS) ConvertTo(dstRaw conversion.Hub) error { log.Printf("ConvertTo: Converting WFS from Spoke version v2beta1 to Hub version v3;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) - return src.ToV3(dst) -} - -//nolint:gosec,funlen,cyclop -func (src *WFS) ToV3(dst *pdoknlv3.WFS) error { - dst.ObjectMeta = src.ObjectMeta - - // Set LifeCycle if defined - if src.Spec.Kubernetes.Lifecycle != nil && src.Spec.Kubernetes.Lifecycle.TTLInDays != nil { - dst.Spec.Lifecycle = &smoothoperatormodel.Lifecycle{ - TTLInDays: smoothoperatorutils.Pointer(int32(*src.Spec.Kubernetes.Lifecycle.TTLInDays)), - } - } - - if src.Spec.Kubernetes.Autoscaling != nil { - dst.Spec.HorizontalPodAutoscalerPatch = ConvertAutoscaling(*src.Spec.Kubernetes.Autoscaling) - } - - if src.Spec.Kubernetes.Resources != nil { - dst.Spec.PodSpecPatch = ConvertResources(*src.Spec.Kubernetes.Resources) - } - - dst.Spec.Options = &ConvertOptionsV2ToV3(src.Spec.Options).BaseOptions - - if src.Spec.Kubernetes.HealthCheck != nil { - dst.Spec.HealthCheck = &pdoknlv3.HealthCheckWFS{ - Querystring: *src.Spec.Kubernetes.HealthCheck.Querystring, - Mimetype: *src.Spec.Kubernetes.HealthCheck.Mimetype, - } - } - - url, err := CreateBaseURL(pdoknlv3.GetHost(true), "wfs", src.Spec.General) - if err != nil { - return err - } - - accessConstraints, err := url.Parse("https://creativecommons.org/publicdomain/zero/1.0/deed.nl") - if err != nil { - return err - } - if src.Spec.Service.AccessConstraints != nil { - accessConstraints, err = url.Parse(*src.Spec.Service.AccessConstraints) - if err != nil { - return err - } - } - - allOtherCrs := []string{ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - } - otherCrs := []string{} - for _, cr := range allOtherCrs { - if cr != src.Spec.Service.DataEPSG { - otherCrs = append(otherCrs, cr) - } - } - - service := pdoknlv3.WFSService{ - BaseService: pdoknlv3.BaseService{ - Prefix: src.Spec.General.Dataset, - URL: *url, - OwnerInfoRef: "pdok", - Title: src.Spec.Service.Title, - Abstract: src.Spec.Service.Abstract, - Keywords: src.Spec.Service.Keywords, - Fees: nil, - AccessConstraints: smoothoperatormodel.URL{URL: accessConstraints}, - }, - DefaultCrs: src.Spec.Service.DataEPSG, - OtherCrs: otherCrs, - FeatureTypes: make([]pdoknlv3.FeatureType, 0), - } - - if src.Spec.Service.Maxfeatures != nil { - maxFeatures, err := strconv.Atoi(*src.Spec.Service.Maxfeatures) - if err != nil { - return err - } - service.CountDefault = &maxFeatures - } - - if src.Spec.Service.Mapfile != nil { - service.Mapfile = &pdoknlv3.Mapfile{ - ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, - } - } - - if src.Spec.Service.Extent != nil && *src.Spec.Service.Extent != "" { - service.Bbox = &pdoknlv3.Bbox{ - DefaultCRS: smoothoperatormodel.ExtentToBBox(*src.Spec.Service.Extent), - } - } else { - service.Bbox = &pdoknlv3.Bbox{ - DefaultCRS: smoothoperatormodel.BBox{ - MinX: "-25000", - MaxX: "280000", - MinY: "250000", - MaxY: "860000", - }, - } - } - - // TODO - where to place the MetadataIdentifier and FeatureTypes[0].SourceMetadataIdentifier if the service is not inspire? - if src.Spec.Service.Inspire { - service.Inspire = &pdoknlv3.WFSInspire{Inspire: pdoknlv3.Inspire{ - ServiceMetadataURL: pdoknlv3.MetadataURL{ - CSW: &pdoknlv3.Metadata{ - MetadataIdentifier: src.Spec.Service.MetadataIdentifier, - }, - }, - Language: "dut"}, - SpatialDatasetIdentifier: src.Spec.Service.FeatureTypes[0].SourceMetadataIdentifier, - } - } - - for _, featureType := range src.Spec.Service.FeatureTypes { - service.FeatureTypes = append(service.FeatureTypes, convertV2FeatureTypeToV3(featureType)) - } - - dst.Spec.Service = service - + // TODO(user): Implement conversion logic from v2beta1 to v3 return nil } -func convertV2FeatureTypeToV3(src FeatureType) pdoknlv3.FeatureType { - featureTypeV3 := pdoknlv3.FeatureType{ - Name: src.Name, - Title: src.Title, - Abstract: src.Abstract, - Keywords: src.Keywords, - DatasetMetadataURL: &pdoknlv3.MetadataURL{ - CSW: &pdoknlv3.Metadata{ - MetadataIdentifier: src.DatasetMetadataIdentifier, - }, - }, - Data: pdoknlv3.BaseData{}, - } - - if src.Extent != nil { - featureTypeV3.Bbox = &pdoknlv3.FeatureBbox{ - DefaultCRS: smoothoperatorutils.Pointer(smoothoperatormodel.ExtentToBBox(*src.Extent)), - } - } - - featureTypeV3.Data = ConvertV2DataToV3(src.Data).BaseData - - return featureTypeV3 -} - // ConvertFrom converts the Hub version (v3) to this WFS (v2beta1). -// -//nolint:revive func (dst *WFS) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*pdoknlv3.WFS) log.Printf("ConvertFrom: Converting WFS from Hub version v3 to Spoke version v2beta1;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) - dst.ObjectMeta = src.ObjectMeta - - dst.Spec.General = LabelsToV2General(src.ObjectMeta.Labels) - - dst.Spec.Kubernetes = NewV2KubernetesObject(src.Spec.Lifecycle, src.Spec.PodSpecPatch, src.Spec.HorizontalPodAutoscalerPatch) - - dst.Spec.Options = ConvertOptionsV3ToV2(&pdoknlv3.Options{BaseOptions: *src.Spec.Options}) - - if src.Spec.HealthCheck != nil { - dst.Spec.Kubernetes.HealthCheck = &HealthCheck{ - Querystring: &src.Spec.HealthCheck.Querystring, - Mimetype: &src.Spec.HealthCheck.Mimetype, - } - } - - accessConstraints := src.Spec.Service.AccessConstraints.String() - - service := WFSService{ - Title: src.Spec.Service.Title, - Abstract: src.Spec.Service.Abstract, - Keywords: src.Spec.Service.Keywords, - AccessConstraints: &accessConstraints, - DataEPSG: src.Spec.Service.DefaultCrs, - Authority: Authority{ - Name: "", - URL: "", - }, - } - - if src.Spec.Service.CountDefault != nil { - service.Maxfeatures = smoothoperatorutils.Pointer(strconv.Itoa(*src.Spec.Service.CountDefault)) - } - - if src.Spec.Service.Bbox != nil { - service.Extent = smoothoperatorutils.Pointer(src.Spec.Service.Bbox.DefaultCRS.ToExtent()) - } else { - service.Extent = smoothoperatorutils.Pointer("-25000 250000 280000 860000") - } - - if src.Spec.Service.Mapfile != nil { - service.Mapfile = &Mapfile{ - ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, - } - } - - if src.Spec.Service.Inspire != nil { - service.Inspire = true - service.MetadataIdentifier = src.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier - } else { - service.Inspire = false - // TODO unable to fill in MetadataIdentifier here until we know how to handle non inspire services - } - - for _, featureType := range src.Spec.Service.FeatureTypes { - featureTypeV2 := FeatureType{ - Name: featureType.Name, - Title: featureType.Title, - Abstract: featureType.Abstract, - Keywords: featureType.Keywords, - DatasetMetadataIdentifier: featureType.DatasetMetadataURL.CSW.MetadataIdentifier, - SourceMetadataIdentifier: "", - Data: ConvertV3DataToV2(pdoknlv3.Data{BaseData: featureType.Data}), - } - - if src.Spec.Service.Inspire != nil { - featureTypeV2.SourceMetadataIdentifier = src.Spec.Service.Inspire.SpatialDatasetIdentifier - } - - if featureType.Bbox != nil { - featureTypeV2.Extent = smoothoperatorutils.Pointer(featureType.Bbox.DefaultCRS.ToExtent()) - } - - service.FeatureTypes = append(service.FeatureTypes, featureTypeV2) - } - - dst.Spec.Service = service - + // TODO(user): Implement conversion logic from v3 to v2beta1 return nil } diff --git a/api/v2beta1/wfs_types.go b/api/v2beta1/wfs_types.go index d349487..c2dfae2 100644 --- a/api/v2beta1/wfs_types.go +++ b/api/v2beta1/wfs_types.go @@ -1,25 +1,17 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v2beta1 @@ -31,16 +23,31 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// WFSSpec defines the desired state of WFS. +type WFSSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of WFS. Edit wfs_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// WFSStatus defines the observed state of WFS. +type WFSStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + // +kubebuilder:object:root=true -// +kubebuilder:skipversion +// +kubebuilder:subresource:status // WFS is the Schema for the wfs API. type WFS struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec WFSSpec `json:"spec,omitempty"` - Status *Status `json:"status,omitempty"` + Spec WFSSpec `json:"spec,omitempty"` + Status WFSStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true @@ -52,44 +59,6 @@ type WFSList struct { Items []WFS `json:"items"` } -// WFSSpec is the struct for all fields defined in the WFS CRD -type WFSSpec struct { - General General `json:"general"` - Service WFSService `json:"service"` - Kubernetes Kubernetes `json:"kubernetes"` - Options *WMSWFSOptions `json:"options,omitempty"` -} - -// WFSService is the struct with all service specific options -type WFSService struct { - Title string `json:"title"` - Inspire bool `json:"inspire"` - Abstract string `json:"abstract"` - // +kubebuilder:default="https://creativecommons.org/publicdomain/zero/1.0/deed.nl" - AccessConstraints *string `json:"accessConstraints,omitempty"` - Keywords []string `json:"keywords"` - MetadataIdentifier string `json:"metadataIdentifier"` - Authority Authority `json:"authority"` - Extent *string `json:"extent,omitempty"` - Maxfeatures *string `json:"maxfeatures,omitempty"` - //nolint:tagliatelle - DataEPSG string `json:"dataEPSG"` - FeatureTypes []FeatureType `json:"featureTypes"` - Mapfile *Mapfile `json:"mapfile,omitempty"` -} - -// FeatureType is the struct for all feature type level fields -type FeatureType struct { - Name string `json:"name"` - Title string `json:"title"` - Abstract string `json:"abstract"` - Keywords []string `json:"keywords"` - DatasetMetadataIdentifier string `json:"datasetMetadataIdentifier"` - SourceMetadataIdentifier string `json:"sourceMetadataIdentifier"` - Extent *string `json:"extent,omitempty"` - Data Data `json:"data"` -} - func init() { SchemeBuilder.Register(&WFS{}, &WFSList{}) } diff --git a/api/v2beta1/wms_conversion.go b/api/v2beta1/wms_conversion.go index 9c8f121..759c837 100644 --- a/api/v2beta1/wms_conversion.go +++ b/api/v2beta1/wms_conversion.go @@ -1,692 +1,45 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v2beta1 import ( - "errors" "log" - "slices" - "strconv" - "strings" - "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/conversion" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatormodel "github.com/pdok/smooth-operator/model" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - - "sigs.k8s.io/controller-runtime/pkg/conversion" ) -const ServiceMetatdataIdentifierAnnotation = "pdok.nl/wms-service-metadata-uuid" - // ConvertTo converts this WMS (v2beta1) to the Hub version (v3). func (src *WMS) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*pdoknlv3.WMS) log.Printf("ConvertTo: Converting WMS from Spoke version v2beta1 to Hub version v3;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) - return src.ToV3(dst) -} - -//nolint:gosec,cyclop,funlen -func (src *WMS) ToV3(target *pdoknlv3.WMS) error { - dst := target - - dst.ObjectMeta = src.ObjectMeta - if dst.Annotations == nil { - dst.Annotations = make(map[string]string) - } - - // Set LifeCycle if defined - if src.Spec.Kubernetes.Lifecycle != nil && src.Spec.Kubernetes.Lifecycle.TTLInDays != nil { - dst.Spec.Lifecycle = &smoothoperatormodel.Lifecycle{ - TTLInDays: smoothoperatorutils.Pointer(int32(*src.Spec.Kubernetes.Lifecycle.TTLInDays)), - } - } - - if src.Spec.Kubernetes.Autoscaling != nil { - dst.Spec.HorizontalPodAutoscalerPatch = ConvertAutoscaling(*src.Spec.Kubernetes.Autoscaling) - } - - if src.Spec.Kubernetes.Resources != nil { - dst.Spec.PodSpecPatch = ConvertResources(*src.Spec.Kubernetes.Resources) - } - - dst.Spec.Options = ConvertOptionsV2ToV3(src.Spec.Options) - dst.Spec.HealthCheck = convertHealthCheckToV3(src.Spec.Kubernetes.HealthCheck) - - url, err := CreateBaseURL(pdoknlv3.GetHost(true), "wms", src.Spec.General) - if err != nil { - return err - } - - accessConstraints, err := url.Parse("https://creativecommons.org/publicdomain/zero/1.0/deed.nl") - if err != nil { - return err - } - if src.Spec.Service.AccessConstraints != nil { - accessConstraints, err = url.Parse(*src.Spec.Service.AccessConstraints) - if err != nil { - return err - } - } - - service := pdoknlv3.WMSService{BaseService: pdoknlv3.BaseService{ - Prefix: src.Spec.General.Dataset, - URL: *url, - OwnerInfoRef: "pdok", - Title: fixUnicode(src.Spec.Service.Title), - Abstract: fixUnicode(src.Spec.Service.Abstract), - Keywords: src.Spec.Service.Keywords, - AccessConstraints: smoothoperatormodel.URL{URL: accessConstraints}, - }, - Inspire: nil, - MaxSize: nil, - Resolution: nil, - DefResolution: nil, - DataEPSG: src.Spec.Service.DataEPSG, - Layer: src.Spec.Service.MapLayersToV3(), - } - - if src.Spec.Service.Maxsize != nil { - service.MaxSize = smoothoperatorutils.Pointer(int32(*src.Spec.Service.Maxsize)) - } - - if src.Spec.Service.Resolution != nil { - service.Resolution = smoothoperatorutils.Pointer(int32(*src.Spec.Service.Resolution)) - } - - if src.Spec.Service.DefResolution != nil { - service.DefResolution = smoothoperatorutils.Pointer(int32(*src.Spec.Service.DefResolution)) - } - - if src.Spec.Service.Mapfile != nil { - service.Mapfile = &pdoknlv3.Mapfile{ - ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, - } - } - - if src.Spec.Service.Inspire { - service.Inspire = &pdoknlv3.Inspire{ - ServiceMetadataURL: pdoknlv3.MetadataURL{ - CSW: &pdoknlv3.Metadata{ - MetadataIdentifier: src.Spec.Service.MetadataIdentifier, - }, - }, - Language: "dut", - } - } else { - // Annotation to be able to convert back to v2 - dst.Annotations[ServiceMetatdataIdentifierAnnotation] = src.Spec.Service.MetadataIdentifier - } - - if src.Spec.Service.StylingAssets != nil { - service.StylingAssets = &pdoknlv3.StylingAssets{ - BlobKeys: src.Spec.Service.StylingAssets.BlobKeys, - ConfigMapRefs: []pdoknlv3.ConfigMapRef{}, - } - - for _, cm := range src.Spec.Service.StylingAssets.ConfigMapRefs { - service.StylingAssets.ConfigMapRefs = append(service.StylingAssets.ConfigMapRefs, pdoknlv3.ConfigMapRef{ - Name: cm.Name, - Keys: cm.Keys, - }) - } - - if len(src.Spec.Service.StylingAssets.ConfigMapRefs) == 1 { - for _, layer := range src.Spec.Service.Layers { - for _, style := range layer.Styles { - if style.Visualization != nil && !slices.Contains(service.StylingAssets.ConfigMapRefs[0].Keys, *style.Visualization) { - service.StylingAssets.ConfigMapRefs[0].Keys = append(service.StylingAssets.ConfigMapRefs[0].Keys, *style.Visualization) - } - } - } - } - } - - dst.Spec.Service = service - return nil -} - -func convertHealthCheckToV3(v2 *HealthCheck) *pdoknlv3.HealthCheckWMS { - if v2 != nil { - switch { - case v2.Querystring != nil: - return &pdoknlv3.HealthCheckWMS{ - Querystring: v2.Querystring, - Mimetype: v2.Mimetype, - } - case v2.Boundingbox != nil: - return &pdoknlv3.HealthCheckWMS{ - Boundingbox: smoothoperatorutils.Pointer(smoothoperatormodel.ExtentToBBox(strings.ReplaceAll(*v2.Boundingbox, ",", " "))), - } - } - } + // TODO(user): Implement conversion logic from v2beta1 to v3 return nil } // ConvertFrom converts the Hub version (v3) to this WMS (v2beta1). -// -//nolint:revive func (dst *WMS) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*pdoknlv3.WMS) log.Printf("ConvertFrom: Converting WMS from Hub version v3 to Spoke version v2beta1;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) - dst.ObjectMeta = src.ObjectMeta - - dst.Spec.General = LabelsToV2General(src.ObjectMeta.Labels) - - dst.Spec.Kubernetes = NewV2KubernetesObject(src.Spec.Lifecycle, src.Spec.PodSpecPatch, src.Spec.HorizontalPodAutoscalerPatch) - dst.Spec.Kubernetes.HealthCheck = convertHealthCheckToV2(src.Spec.HealthCheck) - - dst.Spec.Options = ConvertOptionsV3ToV2(src.Spec.Options) - - service := WMSService{ - Title: src.Spec.Service.Title, - Abstract: src.Spec.Service.Abstract, - Keywords: src.Spec.Service.Keywords, - AccessConstraints: ptr.To(src.Spec.Service.AccessConstraints.String()), - Extent: nil, - DataEPSG: src.Spec.Service.DataEPSG, - Layers: []WMSLayer{}, - MetadataIdentifier: "00000000-0000-0000-0000-000000000000", - } - - if src.Spec.Service.Mapfile != nil { - service.Mapfile = &Mapfile{ - ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, - } - } - - if src.Spec.Service.Inspire != nil { - service.Inspire = true - service.MetadataIdentifier = src.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier - } else { - service.Inspire = false - // TODO unable to fill in MetadataIdentifier here until we know how to handle non inspire services - } - - uuid, ok := src.Annotations[ServiceMetatdataIdentifierAnnotation] - if service.MetadataIdentifier == "00000000-0000-0000-0000-000000000000" && ok { - service.MetadataIdentifier = uuid - } - - if src.Spec.Service.DefResolution != nil { - service.DefResolution = smoothoperatorutils.Pointer(int(*src.Spec.Service.DefResolution)) - } - - if src.Spec.Service.Resolution != nil { - service.Resolution = smoothoperatorutils.Pointer(int(*src.Spec.Service.Resolution)) - } - - if src.Spec.Service.StylingAssets != nil { - service.StylingAssets = &StylingAssets{ - BlobKeys: src.Spec.Service.StylingAssets.BlobKeys, - ConfigMapRefs: []ConfigMapRef{}, - } - - for _, cm := range src.Spec.Service.StylingAssets.ConfigMapRefs { - service.StylingAssets.ConfigMapRefs = append(service.StylingAssets.ConfigMapRefs, ConfigMapRef{ - Name: cm.Name, - Keys: cm.Keys, - }) - } - } - - if v3Authority := src.GetAuthority(); v3Authority != nil { - service.Authority = Authority{ - Name: v3Authority.Name, - URL: v3Authority.URL, - } - } - - if src.Spec.Service.MaxSize != nil { - service.Maxsize = smoothoperatorutils.Pointer(float64(*src.Spec.Service.MaxSize)) - } - - service.Layers = mapV3LayerToV2Layers(src.Spec.Service.Layer, nil, src.Spec.Service.DataEPSG) - - // Create BBox that combines all layer bounding boxes - for _, l := range service.Layers { - if l.Extent != nil { - if service.Extent == nil { - service.Extent = l.Extent - } else { - bbox := smoothoperatorutils.Pointer(smoothoperatormodel.ExtentToBBox(*service.Extent)).DeepCopy() - bbox.Combine(smoothoperatormodel.ExtentToBBox(*l.Extent)) - service.Extent = smoothoperatorutils.Pointer(bbox.ToExtent()) - } - } - } - - dst.Spec.Service = service - - return nil -} - -func convertHealthCheckToV2(v3 *pdoknlv3.HealthCheckWMS) *HealthCheck { - if v3 != nil { - switch { - case v3.Querystring != nil: - return &HealthCheck{ - Querystring: v3.Querystring, - Mimetype: v3.Mimetype, - } - case v3.Boundingbox != nil: - return &HealthCheck{ - Boundingbox: smoothoperatorutils.Pointer(strings.ReplaceAll(v3.Boundingbox.ToExtent(), " ", ",")), - } - } - } - + // TODO(user): Implement conversion logic from v3 to v2beta1 return nil } - -func (v2Service WMSService) GetTopLayer() (*WMSLayer, error) { - // Only one layer defined that has data - if len(v2Service.Layers) == 1 && v2Service.Layers[0].Data != nil { - return nil, nil - } - - // If all layers are groupless there is no toplayer - allGroupless := true - for _, layer := range v2Service.Layers { - if layer.Group != nil && *layer.Group != "" { - allGroupless = false - break - } - } - if allGroupless { - return nil, nil - } - - // Some layers have groups defined. - // That means that there must be one layer without a group, that's the top layer - for _, layer := range v2Service.Layers { - if layer.Group == nil || *layer.Group == "" { - return &layer, nil - } - } - - return nil, errors.New("unable to detect the toplayer of this WMS service") -} - -// MapLayersToV3 -func (v2Service WMSService) MapLayersToV3() pdoknlv3.Layer { - // Creates map of Groups: layers in that group - // and a list of all layers without a group - groupedLayers := map[string][]pdoknlv3.Layer{} - var notGroupedLayers []pdoknlv3.Layer - for _, layer := range v2Service.Layers { - if layer.Group == nil { - notGroupedLayers = append(notGroupedLayers, layer.MapToV3(v2Service)) - } else { - groupedLayers[*layer.Group] = append(groupedLayers[*layer.Group], layer.MapToV3(v2Service)) - } - } - - // if a topLayer is defined in the v2 it be the only layer without a group - // and there are other layers that have the topLayer as their group - // and at least one of those layers is itself a group layer - var topLayer *pdoknlv3.Layer - if _, ok := groupedLayers[*notGroupedLayers[0].Name]; ok && len(notGroupedLayers) == 1 { - subLayers := groupedLayers[*notGroupedLayers[0].Name] - ok := false - for _, layer := range subLayers { - if _, ok = groupedLayers[*layer.Name]; ok { - break - } - } - - if ok { - topLayer = ¬GroupedLayers[0] - var bbox *pdoknlv3.WMSBoundingBox - if len(topLayer.BoundingBoxes) > 0 { - bbox = &topLayer.BoundingBoxes[0] - } - topLayer.BoundingBoxes = getDefaultWMSLayerBoundingBoxes(bbox) - } - } - - var middleLayers []pdoknlv3.Layer - - // if the topLayer is not defined in the v2 layers - // it needs to be created with defaults from the service - // and in this case the middleLayers are all layers without a group - if topLayer == nil { - var bbox *pdoknlv3.WMSBoundingBox - if v2Service.Extent != nil { - bboxStringList := strings.Split(*v2Service.Extent, " ") - bbox = &pdoknlv3.WMSBoundingBox{ - CRS: v2Service.DataEPSG, - BBox: smoothoperatormodel.BBox{ - MinX: bboxStringList[0], - MaxX: bboxStringList[2], - MinY: bboxStringList[1], - MaxY: bboxStringList[3], - }, - } - } - - topLayer = &pdoknlv3.Layer{ - Title: smoothoperatorutils.Pointer(fixUnicode(v2Service.Title)), - Abstract: smoothoperatorutils.Pointer(fixUnicode(v2Service.Abstract)), - Keywords: v2Service.Keywords, - Layers: []pdoknlv3.Layer{}, - BoundingBoxes: getDefaultWMSLayerBoundingBoxes(bbox), - Visible: true, - } - - // adding the bottom layers to the middle layers they are grouped by - for _, layer := range notGroupedLayers { - bottomLayers := groupedLayers[*layer.Name] - layer.Layers = bottomLayers - middleLayers = append(middleLayers, layer) - } - } - - // if the topLayer is defined in the v2 layers - // meaning the topLayer has a name at this point - // then the middleLayers are all layers that had the topLayer name as their group - // and the bottomLayers are all layers that had a middleLayer as a group - if topLayer.Name != nil { - for _, layer := range groupedLayers[*topLayer.Name] { - bottomLayers := groupedLayers[*layer.Name] - layer.Layers = bottomLayers - middleLayers = append(middleLayers, layer) - } - } - topLayer.Layers = middleLayers - - return *topLayer -} - -func getDefaultWMSLayerBoundingBoxes(defaultBbox *pdoknlv3.WMSBoundingBox) []pdoknlv3.WMSBoundingBox { - defaultBboxes := []pdoknlv3.WMSBoundingBox{ - { - CRS: "EPSG:28992", - BBox: smoothoperatormodel.BBox{ - MinX: "-25000", - MinY: "250000", - MaxX: "280000", - MaxY: "860000", - }, - }, - { - CRS: "EPSG:25831", - BBox: smoothoperatormodel.BBox{ - MinX: "-470271", - MinY: "5562310", - MaxX: "795163", - MaxY: "6181970", - }, - }, - { - CRS: "EPSG:25832", - BBox: smoothoperatormodel.BBox{ - MinX: "62461.6", - MinY: "5565550", - MaxX: "397827", - MaxY: "6190420", - }, - }, - { - CRS: "EPSG:3034", - BBox: smoothoperatormodel.BBox{ - MinX: "2613360", - MinY: "3509000", - MaxX: "3220070", - MaxY: "3840030", - }, - }, - { - CRS: "EPSG:3035", - BBox: smoothoperatormodel.BBox{ - MinX: "3016760", - MinY: "3812640", - MaxX: "3644850", - MaxY: "4155860", - }, - }, - { - CRS: "EPSG:3857", - BBox: smoothoperatormodel.BBox{ - MinX: "281318", - MinY: "6483220", - MaxX: "820873", - MaxY: "7503110", - }, - }, - { - CRS: "EPSG:4258", - BBox: smoothoperatormodel.BBox{ - MinX: "50.2129", - MinY: "2.52713", - MaxX: "55.7212", - MaxY: "7.37403", - }, - }, - { - CRS: "EPSG:4326", - BBox: smoothoperatormodel.BBox{ - MinX: "50.2129", - MinY: "2.52713", - MaxX: "55.7212", - MaxY: "7.37403", - }, - }, - { - CRS: "CRS:84", - BBox: smoothoperatormodel.BBox{ - MinX: "2.52713", - MinY: "50.2129", - MaxX: "7.37403", - MaxY: "55.7212", - }, - }, - } - bboxes := []pdoknlv3.WMSBoundingBox{} - if defaultBbox != nil { - bboxes = []pdoknlv3.WMSBoundingBox{*defaultBbox} - } - for _, bbox := range defaultBboxes { - if defaultBbox == nil || bbox.CRS != defaultBbox.CRS { - bboxes = append(bboxes, bbox) - } - } - return bboxes -} - -func (v2Layer WMSLayer) MapToV3(v2Service WMSService) pdoknlv3.Layer { - var abstract *string - if v2Layer.Abstract != nil { - abstract = smoothoperatorutils.Pointer(fixUnicode(*v2Layer.Abstract)) - } - layer := pdoknlv3.Layer{ - Name: &v2Layer.Name, - Title: v2Layer.Title, - Abstract: abstract, - Keywords: v2Layer.Keywords, - LabelNoClip: v2Layer.LabelNoClip, - Styles: []pdoknlv3.Style{}, - Layers: nil, - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - MinScaleDenominator: nil, - MaxScaleDenominator: nil, - Visible: smoothoperatorutils.PointerVal(v2Layer.Visible, true), - } - - if v2Layer.SourceMetadataIdentifier != nil { - layer.Authority = &pdoknlv3.Authority{ - Name: v2Service.Authority.Name, - URL: v2Service.Authority.URL, - SpatialDatasetIdentifier: *v2Layer.SourceMetadataIdentifier, - } - } - - if v2Layer.DatasetMetadataIdentifier != nil { - layer.DatasetMetadataURL = &pdoknlv3.MetadataURL{ - CSW: &pdoknlv3.Metadata{ - MetadataIdentifier: *v2Layer.DatasetMetadataIdentifier, - }, - } - } - - if v2Layer.Extent != nil { - layer.BoundingBoxes = append(layer.BoundingBoxes, pdoknlv3.WMSBoundingBox{ - CRS: v2Service.DataEPSG, - BBox: smoothoperatormodel.ExtentToBBox(*v2Layer.Extent), - }) - } else if v2Service.Extent != nil { - layer.BoundingBoxes = append(layer.BoundingBoxes, pdoknlv3.WMSBoundingBox{ - CRS: v2Service.DataEPSG, - BBox: smoothoperatormodel.ExtentToBBox(*v2Service.Extent), - }) - } - - if v2Layer.MinScale != nil { - layer.MinScaleDenominator = smoothoperatorutils.Pointer(strconv.FormatFloat(*v2Layer.MinScale, 'f', -1, 64)) - } - - if v2Layer.MaxScale != nil { - layer.MaxScaleDenominator = smoothoperatorutils.Pointer(strconv.FormatFloat(*v2Layer.MaxScale, 'f', -1, 64)) - } - - for _, style := range v2Layer.Styles { - v3Style := pdoknlv3.Style{ - Name: style.Name, - Title: style.Title, - Abstract: style.Abstract, - Visualization: style.Visualization, - } - - if style.LegendFile != nil { - v3Style.Legend = &pdoknlv3.Legend{ - BlobKey: style.LegendFile.BlobKey, - } - } - - layer.Styles = append(layer.Styles, v3Style) - } - - if v2Layer.Data != nil { - layer.Data = smoothoperatorutils.Pointer(ConvertV2DataToV3(*v2Layer.Data)) - } - - return layer -} - -//nolint:cyclop -func mapV3LayerToV2Layers(v3Layer pdoknlv3.Layer, parent *pdoknlv3.Layer, serviceEPSG string) []WMSLayer { - var layers []WMSLayer - - //nolint:nestif - if parent == nil && v3Layer.Name == nil { - // Default top layer, do not include in v2 layers - if v3Layer.Layers != nil { - for _, childLayer := range v3Layer.Layers { - layers = append(layers, mapV3LayerToV2Layers(childLayer, nil, serviceEPSG)...) - } - } - } else { - v2Layer := WMSLayer{ - Name: *v3Layer.Name, - Title: v3Layer.Title, - Abstract: v3Layer.Abstract, - Keywords: v3Layer.Keywords, - LabelNoClip: v3Layer.LabelNoClip, - Styles: []Style{}, - } - - v2Layer.Visible = &v3Layer.Visible - - if parent != nil { - v2Layer.Group = parent.Name - } - - if v3Layer.DatasetMetadataURL != nil && v3Layer.DatasetMetadataURL.CSW != nil { - v2Layer.DatasetMetadataIdentifier = &v3Layer.DatasetMetadataURL.CSW.MetadataIdentifier - } - - if v3Layer.Authority != nil { - v2Layer.SourceMetadataIdentifier = &v3Layer.Authority.SpatialDatasetIdentifier - } - - for _, bb := range v3Layer.BoundingBoxes { - if bb.CRS == serviceEPSG { - v2Layer.Extent = smoothoperatorutils.Pointer(bb.BBox.ToExtent()) - } - } - - if v3Layer.MinScaleDenominator != nil { - val, err := strconv.ParseFloat(*v3Layer.MinScaleDenominator, 64) - if err != nil { - panic(err) - } - v2Layer.MinScale = &val - } - - if v3Layer.MaxScaleDenominator != nil { - val, err := strconv.ParseFloat(*v3Layer.MaxScaleDenominator, 64) - if err != nil { - panic(err) - } - v2Layer.MaxScale = &val - } - - for _, v3Style := range v3Layer.Styles { - v2Style := Style{ - Name: v3Style.Name, - Title: v3Style.Title, - Abstract: v3Style.Abstract, - Visualization: v3Style.Visualization, - } - - if v3Style.Legend != nil { - v2Style.LegendFile = &LegendFile{ - BlobKey: v3Style.Legend.BlobKey, - } - } - - v2Layer.Styles = append(v2Layer.Styles, v2Style) - } - - if v3Layer.Data != nil { - v2Layer.Data = smoothoperatorutils.Pointer(ConvertV3DataToV2(*v3Layer.Data)) - } - - layers = append(layers, v2Layer) - - if v3Layer.Layers != nil { - for _, childLayer := range v3Layer.Layers { - layers = append(layers, mapV3LayerToV2Layers(childLayer, &v3Layer, serviceEPSG)...) - } - } - } - - return layers -} diff --git a/api/v2beta1/wms_conversion_test.go b/api/v2beta1/wms_conversion_test.go deleted file mode 100644 index 0305958..0000000 --- a/api/v2beta1/wms_conversion_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package v2beta1 - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "k8s.io/utils/ptr" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/stretchr/testify/assert" - "sigs.k8s.io/yaml" -) - -func TestV2ToV3(t *testing.T) { - //nolint:misspell - input := "apiVersion: pdok.nl/v2beta1\nkind: WMS\nmetadata:\n name: rws-nwbwegen-v1-0\n labels:\n dataset-owner: rws\n dataset: nwbwegen\n service-version: v1_0\n service-type: wms\n annotations:\n lifecycle-phase: prod\n service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb\nspec:\n general:\n datasetOwner: rws\n dataset: nwbwegen\n serviceVersion: v1_0\n kubernetes:\n healthCheck:\n boundingbox: 135134.89,457152.55,135416.03,457187.82\n resources:\n limits:\n ephemeralStorage: 1535Mi\n memory: 4G\n requests:\n cpu: 2000m\n ephemeralStorage: 1535Mi\n memory: 4G\n options:\n automaticCasing: true\n disableWebserviceProxy: false\n includeIngress: true\n validateRequests: true\n service:\n title: NWB - Wegen WMS\n abstract:\n Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen.\n Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen\n Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland.\n Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk,\n provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien\n van een straatnaam of nummer.\n authority:\n name: rws\n url: https://www.rijkswaterstaat.nl\n dataEPSG: EPSG:28992\n extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961\n inspire: true\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n - Hectometerpunten\n - HVD\n - Mobiliteit\n stylingAssets:\n configMapRefs:\n - name: includes\n keys:\n - nwb_wegen_hectopunten.symbol\n - hectopunten.style\n - wegvakken.style\n blobKeys:\n - resources/fonts/liberation-sans.ttf\n layers:\n - abstract:\n Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB)\n en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer,\n routenummer, wegbeheerder, huisnummers, enz. weer.\n data:\n gpkg:\n columns:\n - objectid\n - wvk_id\n - wvk_begdat\n - jte_id_beg\n - jte_id_end\n - wegbehsrt\n - wegnummer\n - wegdeelltr\n - hecto_lttr\n - bst_code\n - rpe_code\n - admrichtng\n - rijrichtng\n - stt_naam\n - stt_bron\n - wpsnaam\n - gme_id\n - gme_naam\n - hnrstrlnks\n - hnrstrrhts\n - e_hnr_lnks\n - e_hnr_rhts\n - l_hnr_lnks\n - l_hnr_rhts\n - begafstand\n - endafstand\n - beginkm\n - eindkm\n - pos_tv_wol\n - wegbehcode\n - wegbehnaam\n - distrcode\n - distrnaam\n - dienstcode\n - dienstnaam\n - wegtype\n - wgtype_oms\n - routeltr\n - routenr\n - routeltr2\n - routenr2\n - routeltr3\n - routenr3\n - routeltr4\n - routenr4\n - wegnr_aw\n - wegnr_hmp\n - geobron_id\n - geobron_nm\n - bronjaar\n - openlr\n - bag_orl\n - frc\n - fow\n - alt_naam\n - alt_nr\n - rel_hoogte\n - st_lengthshape\n geometryType: MultiLineString\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: wegvakken\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n maxScale: 50000.0\n minScale: 1.0\n name: wegvakken\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: wegvakken\n title: NWB - Wegvakken\n visualization: wegvakken.style\n title: Wegvakken\n visible: true\n - abstract:\n Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB)\n en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand,\n zijde en hectoletter weer.\n data:\n gpkg:\n columns:\n - objectid\n - hectomtrng\n - afstand\n - wvk_id\n - wvk_begdat\n - zijde\n - hecto_lttr\n geometryType: MultiPoint\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: hectopunten\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Hectometerpunten\n maxScale: 50000.0\n minScale: 1.0\n name: hectopunten\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: hectopunten\n title: NWB - Hectopunten\n visualization: hectopunten.style\n title: Hectopunten\n visible: true\n metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8\n" - v2wms := &WMS{} - err := yaml.Unmarshal([]byte(input), v2wms) - assert.NoError(t, err) - var target pdoknlv3.WMS - err = v2wms.ToV3(&target) - assert.NoError(t, err) - assert.Equal(t, "NWB - Wegen WMS", target.Spec.Service.Title) - a := 0 - _ = a -} - -func TestWMSService_MapLayersToV3(t *testing.T) { - tests := []struct { - name string - v2Service WMSService - want pdoknlv3.Layer - }{ - { - name: "no toplayer, middle: 1 data layer", - v2Service: WMSService{Layers: []WMSLayer{ - {Name: "layer"}, - }}, - want: pdoknlv3.Layer{ - Title: ptr.To(""), - Abstract: ptr.To(""), - BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), - Visible: true, - Layers: []pdoknlv3.Layer{{ - Name: ptr.To("layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }}, - }, - }, - { - name: "no toplayer, middle: 1 group layer", - v2Service: WMSService{Layers: []WMSLayer{ - {Name: "group-layer"}, - {Name: "sub-layer", Group: ptr.To("group-layer")}, - }}, - want: pdoknlv3.Layer{ - Title: ptr.To(""), - Abstract: ptr.To(""), - BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), - Visible: true, - Layers: []pdoknlv3.Layer{{ - Name: ptr.To("group-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("sub-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }}, - }, - }, - { - name: "no toplayer, middle: 2 group layers", - v2Service: WMSService{Layers: []WMSLayer{ - {Name: "group-layer-1"}, - {Name: "sub-layer-1", Group: ptr.To("group-layer-1")}, - {Name: "group-layer-2"}, - {Name: "sub-layer-2", Group: ptr.To("group-layer-2")}, - }}, - want: pdoknlv3.Layer{ - Title: ptr.To(""), - Abstract: ptr.To(""), - BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), - Visible: true, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("group-layer-1"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("sub-layer-1"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }, - { - Name: ptr.To("group-layer-2"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("sub-layer-2"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }, - }, - }, - }, - { - name: "no toplayer, middle: 1 group layer, 1 data layer", - v2Service: WMSService{Layers: []WMSLayer{ - {Name: "group-layer"}, - {Name: "sub-layer", Group: ptr.To("group-layer")}, - {Name: "data-layer"}, - }}, - want: pdoknlv3.Layer{ - Title: ptr.To(""), - Abstract: ptr.To(""), - BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), - Visible: true, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("group-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("sub-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }, - { - Name: ptr.To("data-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }, - }, - { - name: "toplayer, middle: 1 group layer", - v2Service: WMSService{Layers: []WMSLayer{ - {Name: "group-layer", Group: ptr.To("top-layer")}, - {Name: "sub-layer", Group: ptr.To("group-layer")}, - {Name: "top-layer"}, - }}, - want: pdoknlv3.Layer{ - Name: ptr.To("top-layer"), - BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{{ - Name: ptr.To("group-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("sub-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }}, - }, - }, - { - name: "toplayer, middle: 1 group layer, 1 data layer", - v2Service: WMSService{Layers: []WMSLayer{ - {Name: "group-layer", Group: ptr.To("top-layer")}, - {Name: "sub-layer", Group: ptr.To("group-layer")}, - {Name: "top-layer"}, - {Name: "data-layer", Group: ptr.To("top-layer")}, - }}, - want: pdoknlv3.Layer{ - Name: ptr.To("top-layer"), - BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("group-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - Layers: []pdoknlv3.Layer{ - { - Name: ptr.To("sub-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }, - { - Name: ptr.To("data-layer"), - BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, - Visible: true, - Styles: []pdoknlv3.Style{}, - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - diff := cmp.Diff(tt.want, tt.v2Service.MapLayersToV3()) - assert.Equal(t, diff == "", true, "%s", diff) - }) - } -} diff --git a/api/v2beta1/wms_types.go b/api/v2beta1/wms_types.go index 2d7a160..f5918a6 100644 --- a/api/v2beta1/wms_types.go +++ b/api/v2beta1/wms_types.go @@ -1,126 +1,53 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v2beta1 import ( - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -// +kubebuilder:object:root=true -// +kubebuilder:skipversion - -// WMS is the Schema for the wms API. -type WMS struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec WMSSpec `json:"spec,omitempty"` - Status *Status `json:"status,omitempty"` -} - -// WMSSpec is the struct for all fields defined in the WMS CRD +// WMSSpec defines the desired state of WMS. type WMSSpec struct { - General General `json:"general"` - Service WMSService `json:"service"` - Options *WMSWFSOptions `json:"options,omitempty"` - Kubernetes Kubernetes `json:"kubernetes"` -} + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file -// WMSService is the struct for all service level fields -type WMSService struct { - Inspire bool `json:"inspire,omitempty"` - Title string `json:"title"` - Abstract string `json:"abstract"` - // +kubebuilder:default="https://creativecommons.org/publicdomain/zero/1.0/deed.nl" - AccessConstraints *string `json:"accessConstraints,omitempty"` // Pointer for CRD conversion as defaulting is not applied there - Keywords []string `json:"keywords"` - MetadataIdentifier string `json:"metadataIdentifier"` - Authority Authority `json:"authority"` - Layers []WMSLayer `json:"layers"` - //nolint:tagliatelle - DataEPSG string `json:"dataEPSG"` - Extent *string `json:"extent,omitempty"` - Maxsize *float64 `json:"maxSize,omitempty"` - Resolution *int `json:"resolution,omitempty"` - DefResolution *int `json:"defResolution,omitempty"` - StylingAssets *StylingAssets `json:"stylingAssets,omitempty"` - Mapfile *Mapfile `json:"mapfile,omitempty"` + // Foo is an example field of WMS. Edit wms_types.go to remove/update + Foo string `json:"foo,omitempty"` } -// WMSLayer is the struct for all layer level fields -type WMSLayer struct { - Name string `json:"name"` - Group *string `json:"group,omitempty"` - Visible *bool `json:"visible,omitempty"` - Title *string `json:"title,omitempty"` - Abstract *string `json:"abstract,omitempty"` - Keywords []string `json:"keywords,omitempty"` - DatasetMetadataIdentifier *string `json:"datasetMetadataIdentifier,omitempty"` - SourceMetadataIdentifier *string `json:"sourceMetadataIdentifier,omitempty"` - Styles []Style `json:"styles"` - Extent *string `json:"extent,omitempty"` - MinScale *float64 `json:"minScale,omitempty"` - MaxScale *float64 `json:"maxScale,omitempty"` - LabelNoClip bool `json:"labelNoClip,omitempty"` - Data *Data `json:"data,omitempty"` +// WMSStatus defines the observed state of WMS. +type WMSStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file } -// Style is the struct for all style level fields -type Style struct { - Name string `json:"name"` - Title *string `json:"title,omitempty"` - Abstract *string `json:"abstract,omitempty"` - Visualization *string `json:"visualization,omitempty"` - LegendFile *LegendFile `json:"legendFile,omitempty"` -} - -// LegendFile is the struct containing the location of the legendfile -type LegendFile struct { - BlobKey string `json:"blobKey"` -} - -// StylingAssets is the struct containing the location of styling assets -type StylingAssets struct { - ConfigMapRefs []ConfigMapRef `json:"configMapRefs,omitempty"` - BlobKeys []string `json:"blobKeys,omitempty"` -} +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status -// ConfigMapRef contains all the config map name and all keys in that mapserver that are relevant -// the Keys can be empty, so that the v1 WMS can convert to the v2beta1 WMS -type ConfigMapRef struct { - Name string `json:"name"` - Keys []string `json:"keys,omitempty"` -} +// WMS is the Schema for the wms API. +type WMS struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` -// Mapfile contains the ConfigMapKeyRef containing a mapfile -type Mapfile struct { - ConfigMapKeyRef corev1.ConfigMapKeySelector `json:"configMapKeyRef"` + Spec WMSSpec `json:"spec,omitempty"` + Status WMSStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v2beta1/zz_generated.deepcopy.go b/api/v2beta1/zz_generated.deepcopy.go index 7adf7ea..4975d06 100644 --- a/api/v2beta1/zz_generated.deepcopy.go +++ b/api/v2beta1/zz_generated.deepcopy.go @@ -1,27 +1,19 @@ //go:build !ignore_autogenerated /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ // Code generated by controller-gen. DO NOT EDIT. @@ -29,534 +21,16 @@ SOFTWARE. package v2beta1 import ( - "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Authority) DeepCopyInto(out *Authority) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authority. -func (in *Authority) DeepCopy() *Authority { - if in == nil { - return nil - } - out := new(Authority) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Autoscaling) DeepCopyInto(out *Autoscaling) { - *out = *in - if in.AverageCPUUtilization != nil { - in, out := &in.AverageCPUUtilization, &out.AverageCPUUtilization - *out = new(int) - **out = **in - } - if in.MinReplicas != nil { - in, out := &in.MinReplicas, &out.MinReplicas - *out = new(int) - **out = **in - } - if in.MaxReplicas != nil { - in, out := &in.MaxReplicas, &out.MaxReplicas - *out = new(int) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Autoscaling. -func (in *Autoscaling) DeepCopy() *Autoscaling { - if in == nil { - return nil - } - out := new(Autoscaling) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Condition) DeepCopyInto(out *Condition) { - *out = *in - in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) - if in.AnsibleResult != nil { - in, out := &in.AnsibleResult, &out.AnsibleResult - *out = new(ResultAnsible) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. -func (in *Condition) DeepCopy() *Condition { - if in == nil { - return nil - } - out := new(Condition) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConfigMapRef) DeepCopyInto(out *ConfigMapRef) { - *out = *in - if in.Keys != nil { - in, out := &in.Keys, &out.Keys - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapRef. -func (in *ConfigMapRef) DeepCopy() *ConfigMapRef { - if in == nil { - return nil - } - out := new(ConfigMapRef) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Data) DeepCopyInto(out *Data) { - *out = *in - if in.GPKG != nil { - in, out := &in.GPKG, &out.GPKG - *out = new(GPKG) - (*in).DeepCopyInto(*out) - } - if in.Postgis != nil { - in, out := &in.Postgis, &out.Postgis - *out = new(Postgis) - (*in).DeepCopyInto(*out) - } - if in.Tif != nil { - in, out := &in.Tif, &out.Tif - *out = new(Tif) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Data. -func (in *Data) DeepCopy() *Data { - if in == nil { - return nil - } - out := new(Data) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FeatureType) DeepCopyInto(out *FeatureType) { - *out = *in - if in.Keywords != nil { - in, out := &in.Keywords, &out.Keywords - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Extent != nil { - in, out := &in.Extent, &out.Extent - *out = new(string) - **out = **in - } - in.Data.DeepCopyInto(&out.Data) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureType. -func (in *FeatureType) DeepCopy() *FeatureType { - if in == nil { - return nil - } - out := new(FeatureType) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *GPKG) DeepCopyInto(out *GPKG) { - *out = *in - if in.Columns != nil { - in, out := &in.Columns, &out.Columns - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Aliases != nil { - in, out := &in.Aliases, &out.Aliases - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GPKG. -func (in *GPKG) DeepCopy() *GPKG { - if in == nil { - return nil - } - out := new(GPKG) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *General) DeepCopyInto(out *General) { - *out = *in - if in.Theme != nil { - in, out := &in.Theme, &out.Theme - *out = new(string) - **out = **in - } - if in.ServiceVersion != nil { - in, out := &in.ServiceVersion, &out.ServiceVersion - *out = new(string) - **out = **in - } - if in.DataVersion != nil { - in, out := &in.DataVersion, &out.DataVersion - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new General. -func (in *General) DeepCopy() *General { - if in == nil { - return nil - } - out := new(General) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HealthCheck) DeepCopyInto(out *HealthCheck) { - *out = *in - if in.Querystring != nil { - in, out := &in.Querystring, &out.Querystring - *out = new(string) - **out = **in - } - if in.Mimetype != nil { - in, out := &in.Mimetype, &out.Mimetype - *out = new(string) - **out = **in - } - if in.Boundingbox != nil { - in, out := &in.Boundingbox, &out.Boundingbox - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheck. -func (in *HealthCheck) DeepCopy() *HealthCheck { - if in == nil { - return nil - } - out := new(HealthCheck) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Kubernetes) DeepCopyInto(out *Kubernetes) { - *out = *in - if in.Autoscaling != nil { - in, out := &in.Autoscaling, &out.Autoscaling - *out = new(Autoscaling) - (*in).DeepCopyInto(*out) - } - if in.HealthCheck != nil { - in, out := &in.HealthCheck, &out.HealthCheck - *out = new(HealthCheck) - (*in).DeepCopyInto(*out) - } - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = new(v1.ResourceRequirements) - (*in).DeepCopyInto(*out) - } - if in.Lifecycle != nil { - in, out := &in.Lifecycle, &out.Lifecycle - *out = new(Lifecycle) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kubernetes. -func (in *Kubernetes) DeepCopy() *Kubernetes { - if in == nil { - return nil - } - out := new(Kubernetes) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LegendFile) DeepCopyInto(out *LegendFile) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LegendFile. -func (in *LegendFile) DeepCopy() *LegendFile { - if in == nil { - return nil - } - out := new(LegendFile) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Lifecycle) DeepCopyInto(out *Lifecycle) { - *out = *in - if in.TTLInDays != nil { - in, out := &in.TTLInDays, &out.TTLInDays - *out = new(int) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lifecycle. -func (in *Lifecycle) DeepCopy() *Lifecycle { - if in == nil { - return nil - } - out := new(Lifecycle) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Mapfile) DeepCopyInto(out *Mapfile) { - *out = *in - in.ConfigMapKeyRef.DeepCopyInto(&out.ConfigMapKeyRef) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mapfile. -func (in *Mapfile) DeepCopy() *Mapfile { - if in == nil { - return nil - } - out := new(Mapfile) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Postgis) DeepCopyInto(out *Postgis) { - *out = *in - if in.Columns != nil { - in, out := &in.Columns, &out.Columns - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Aliases != nil { - in, out := &in.Aliases, &out.Aliases - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgis. -func (in *Postgis) DeepCopy() *Postgis { - if in == nil { - return nil - } - out := new(Postgis) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Resources) DeepCopyInto(out *Resources) { - *out = *in - if in.APIVersion != nil { - in, out := &in.APIVersion, &out.APIVersion - *out = new(string) - **out = **in - } - if in.Kind != nil { - in, out := &in.Kind, &out.Kind - *out = new(string) - **out = **in - } - if in.Name != nil { - in, out := &in.Name, &out.Name - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources. -func (in *Resources) DeepCopy() *Resources { - if in == nil { - return nil - } - out := new(Resources) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResultAnsible) DeepCopyInto(out *ResultAnsible) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResultAnsible. -func (in *ResultAnsible) DeepCopy() *ResultAnsible { - if in == nil { - return nil - } - out := new(ResultAnsible) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Status) DeepCopyInto(out *Status) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Deployment != nil { - in, out := &in.Deployment, &out.Deployment - *out = new(string) - **out = **in - } - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = make([]Resources, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. -func (in *Status) DeepCopy() *Status { - if in == nil { - return nil - } - out := new(Status) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Style) DeepCopyInto(out *Style) { - *out = *in - if in.Title != nil { - in, out := &in.Title, &out.Title - *out = new(string) - **out = **in - } - if in.Abstract != nil { - in, out := &in.Abstract, &out.Abstract - *out = new(string) - **out = **in - } - if in.Visualization != nil { - in, out := &in.Visualization, &out.Visualization - *out = new(string) - **out = **in - } - if in.LegendFile != nil { - in, out := &in.LegendFile, &out.LegendFile - *out = new(LegendFile) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Style. -func (in *Style) DeepCopy() *Style { - if in == nil { - return nil - } - out := new(Style) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *StylingAssets) DeepCopyInto(out *StylingAssets) { - *out = *in - if in.ConfigMapRefs != nil { - in, out := &in.ConfigMapRefs, &out.ConfigMapRefs - *out = make([]ConfigMapRef, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.BlobKeys != nil { - in, out := &in.BlobKeys, &out.BlobKeys - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StylingAssets. -func (in *StylingAssets) DeepCopy() *StylingAssets { - if in == nil { - return nil - } - out := new(StylingAssets) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Tif) DeepCopyInto(out *Tif) { - *out = *in - if in.GetFeatureInfoIncludesClass != nil { - in, out := &in.GetFeatureInfoIncludesClass, &out.GetFeatureInfoIncludesClass - *out = new(bool) - **out = **in - } - if in.Offsite != nil { - in, out := &in.Offsite, &out.Offsite - *out = new(string) - **out = **in - } - if in.Resample != nil { - in, out := &in.Resample, &out.Resample - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tif. -func (in *Tif) DeepCopy() *Tif { - if in == nil { - return nil - } - out := new(Tif) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WFS) DeepCopyInto(out *WFS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - if in.Status != nil { - in, out := &in.Status, &out.Status - *out = new(Status) - (*in).DeepCopyInto(*out) - } + out.Spec = in.Spec + out.Status = in.Status } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFS. @@ -610,72 +84,31 @@ func (in *WFSList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSService) DeepCopyInto(out *WFSService) { +func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { *out = *in - if in.AccessConstraints != nil { - in, out := &in.AccessConstraints, &out.AccessConstraints - *out = new(string) - **out = **in - } - if in.Keywords != nil { - in, out := &in.Keywords, &out.Keywords - *out = make([]string, len(*in)) - copy(*out, *in) - } - out.Authority = in.Authority - if in.Extent != nil { - in, out := &in.Extent, &out.Extent - *out = new(string) - **out = **in - } - if in.Maxfeatures != nil { - in, out := &in.Maxfeatures, &out.Maxfeatures - *out = new(string) - **out = **in - } - if in.FeatureTypes != nil { - in, out := &in.FeatureTypes, &out.FeatureTypes - *out = make([]FeatureType, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Mapfile != nil { - in, out := &in.Mapfile, &out.Mapfile - *out = new(Mapfile) - (*in).DeepCopyInto(*out) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSService. -func (in *WFSService) DeepCopy() *WFSService { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. +func (in *WFSSpec) DeepCopy() *WFSSpec { if in == nil { return nil } - out := new(WFSService) + out := new(WFSSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { +func (in *WFSStatus) DeepCopyInto(out *WFSStatus) { *out = *in - in.General.DeepCopyInto(&out.General) - in.Service.DeepCopyInto(&out.Service) - in.Kubernetes.DeepCopyInto(&out.Kubernetes) - if in.Options != nil { - in, out := &in.Options, &out.Options - *out = new(WMSWFSOptions) - (*in).DeepCopyInto(*out) - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. -func (in *WFSSpec) DeepCopy() *WFSSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSStatus. +func (in *WFSStatus) DeepCopy() *WFSStatus { if in == nil { return nil } - out := new(WFSSpec) + out := new(WFSStatus) in.DeepCopyInto(out) return out } @@ -685,12 +118,8 @@ func (in *WMS) DeepCopyInto(out *WMS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - if in.Status != nil { - in, out := &in.Status, &out.Status - *out = new(Status) - (*in).DeepCopyInto(*out) - } + out.Spec = in.Spec + out.Status = in.Status } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMS. @@ -711,83 +140,6 @@ func (in *WMS) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSLayer) DeepCopyInto(out *WMSLayer) { - *out = *in - if in.Group != nil { - in, out := &in.Group, &out.Group - *out = new(string) - **out = **in - } - if in.Visible != nil { - in, out := &in.Visible, &out.Visible - *out = new(bool) - **out = **in - } - if in.Title != nil { - in, out := &in.Title, &out.Title - *out = new(string) - **out = **in - } - if in.Abstract != nil { - in, out := &in.Abstract, &out.Abstract - *out = new(string) - **out = **in - } - if in.Keywords != nil { - in, out := &in.Keywords, &out.Keywords - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.DatasetMetadataIdentifier != nil { - in, out := &in.DatasetMetadataIdentifier, &out.DatasetMetadataIdentifier - *out = new(string) - **out = **in - } - if in.SourceMetadataIdentifier != nil { - in, out := &in.SourceMetadataIdentifier, &out.SourceMetadataIdentifier - *out = new(string) - **out = **in - } - if in.Styles != nil { - in, out := &in.Styles, &out.Styles - *out = make([]Style, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Extent != nil { - in, out := &in.Extent, &out.Extent - *out = new(string) - **out = **in - } - if in.MinScale != nil { - in, out := &in.MinScale, &out.MinScale - *out = new(float64) - **out = **in - } - if in.MaxScale != nil { - in, out := &in.MaxScale, &out.MaxScale - *out = new(float64) - **out = **in - } - if in.Data != nil { - in, out := &in.Data, &out.Data - *out = new(Data) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSLayer. -func (in *WMSLayer) DeepCopy() *WMSLayer { - if in == nil { - return nil - } - out := new(WMSLayer) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WMSList) DeepCopyInto(out *WMSList) { *out = *in @@ -820,80 +172,9 @@ func (in *WMSList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSService) DeepCopyInto(out *WMSService) { - *out = *in - if in.AccessConstraints != nil { - in, out := &in.AccessConstraints, &out.AccessConstraints - *out = new(string) - **out = **in - } - if in.Keywords != nil { - in, out := &in.Keywords, &out.Keywords - *out = make([]string, len(*in)) - copy(*out, *in) - } - out.Authority = in.Authority - if in.Layers != nil { - in, out := &in.Layers, &out.Layers - *out = make([]WMSLayer, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Extent != nil { - in, out := &in.Extent, &out.Extent - *out = new(string) - **out = **in - } - if in.Maxsize != nil { - in, out := &in.Maxsize, &out.Maxsize - *out = new(float64) - **out = **in - } - if in.Resolution != nil { - in, out := &in.Resolution, &out.Resolution - *out = new(int) - **out = **in - } - if in.DefResolution != nil { - in, out := &in.DefResolution, &out.DefResolution - *out = new(int) - **out = **in - } - if in.StylingAssets != nil { - in, out := &in.StylingAssets, &out.StylingAssets - *out = new(StylingAssets) - (*in).DeepCopyInto(*out) - } - if in.Mapfile != nil { - in, out := &in.Mapfile, &out.Mapfile - *out = new(Mapfile) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSService. -func (in *WMSService) DeepCopy() *WMSService { - if in == nil { - return nil - } - out := new(WMSService) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WMSSpec) DeepCopyInto(out *WMSSpec) { *out = *in - in.General.DeepCopyInto(&out.General) - in.Service.DeepCopyInto(&out.Service) - if in.Options != nil { - in, out := &in.Options, &out.Options - *out = new(WMSWFSOptions) - (*in).DeepCopyInto(*out) - } - in.Kubernetes.DeepCopyInto(&out.Kubernetes) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSSpec. @@ -907,41 +188,16 @@ func (in *WMSSpec) DeepCopy() *WMSSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSWFSOptions) DeepCopyInto(out *WMSWFSOptions) { +func (in *WMSStatus) DeepCopyInto(out *WMSStatus) { *out = *in - if in.ValidateRequests != nil { - in, out := &in.ValidateRequests, &out.ValidateRequests - *out = new(bool) - **out = **in - } - if in.RewriteGroupToDataLayers != nil { - in, out := &in.RewriteGroupToDataLayers, &out.RewriteGroupToDataLayers - *out = new(bool) - **out = **in - } - if in.DisableWebserviceProxy != nil { - in, out := &in.DisableWebserviceProxy, &out.DisableWebserviceProxy - *out = new(bool) - **out = **in - } - if in.PrefetchData != nil { - in, out := &in.PrefetchData, &out.PrefetchData - *out = new(bool) - **out = **in - } - if in.ValidateChildStyleNameEqual != nil { - in, out := &in.ValidateChildStyleNameEqual, &out.ValidateChildStyleNameEqual - *out = new(bool) - **out = **in - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSWFSOptions. -func (in *WMSWFSOptions) DeepCopy() *WMSWFSOptions { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSStatus. +func (in *WMSStatus) DeepCopy() *WMSStatus { if in == nil { return nil } - out := new(WMSWFSOptions) + out := new(WMSStatus) in.DeepCopyInto(out) return out } diff --git a/api/v3/groupversion_info.go b/api/v3/groupversion_info.go index 9262d83..a616013 100644 --- a/api/v3/groupversion_info.go +++ b/api/v3/groupversion_info.go @@ -1,25 +1,17 @@ /* -MIT License - -Copyright (c) 2024 Publieke Dienstverlening op de Kaart - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ // Package v3 contains API Schema definitions for the v3 API group. @@ -28,7 +20,6 @@ SOFTWARE. package v3 import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) @@ -42,14 +33,4 @@ var ( // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme - - TypeMetaWFS = metav1.TypeMeta{ - Kind: "WFS", - APIVersion: GroupVersion.String(), - } - - TypeMetaWMS = metav1.TypeMeta{ - Kind: "WMS", - APIVersion: GroupVersion.String(), - } ) diff --git a/api/v3/shared_types.go b/api/v3/shared_types.go deleted file mode 100644 index ff27831..0000000 --- a/api/v3/shared_types.go +++ /dev/null @@ -1,329 +0,0 @@ -package v3 - -import ( - "strings" - - smoothoperatormodel "github.com/pdok/smooth-operator/model" - - "k8s.io/apimachinery/pkg/runtime/schema" - - autoscalingv2 "k8s.io/api/autoscaling/v2" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var host string - -type ServiceType string - -const ( - ServiceTypeWMS ServiceType = "WMS" - ServiceTypeWFS ServiceType = "WFS" -) - -// HorizontalPodAutoscalerPatch - copy of autoscalingv2.HorizontalPodAutoscalerSpec without ScaleTargetRef -// This way we don't have to specify the scaleTargetRef field in the CRD. -type HorizontalPodAutoscalerPatch struct { - MinReplicas *int32 `json:"minReplicas,omitempty"` - MaxReplicas *int32 `json:"maxReplicas,omitempty"` - Metrics []autoscalingv2.MetricSpec `json:"metrics,omitempty"` - Behavior *autoscalingv2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"` -} - -// WMSWFS is the common interface used for both WMS and WFS resources. -// +kubebuilder:object:generate=false -type WMSWFS interface { - *WFS | *WMS - metav1.Object - - GroupKind() schema.GroupKind - Inspire() *WFSInspire - Mapfile() *Mapfile - PodSpecPatch() corev1.PodSpec - HorizontalPodAutoscalerPatch() *HorizontalPodAutoscalerPatch - Type() ServiceType - TypedName() string - Options() Options - HasPostgisData() bool - OwnerInfoRef() string - - // URL returns the configured service URL - URL() smoothoperatormodel.URL - IngressRouteURLs(includeServiceURLWhenEmpty bool) smoothoperatormodel.IngressRouteURLs - - // DatasetMetadataIds returns list of all configured metadata identifiers configured on Layers or Featuretypes - DatasetMetadataIDs() []string - - GeoPackages() []*Gpkg - - ReadinessQueryString() (string, string, error) -} - -// Mapfile references a ConfigMap key where an external mapfile is stored. -// +kubebuilder:validation:Type=object -type Mapfile struct { - // +kubebuilder:validation:Type=object - ConfigMapKeyRef corev1.ConfigMapKeySelector `json:"configMapKeyRef"` -} - -// BaseOptions for all apis -type BaseOptions struct { - // IncludeIngress dictates whether to deploy an Ingress or ensure none exists. - // +kubebuilder:default:=true - // +kubebuilder:validation:Optional - IncludeIngress bool `json:"includeIngress"` - - // AutomaticCasing enables automatic conversion from snake_case to camelCase. - // +kubebuilder:default:=true - // +kubebuilder:validation:Optional - AutomaticCasing bool `json:"automaticCasing"` - - // Whether to prefetch data from blob storage, and store it on the local filesystem. - // If `false`, the data will be served directly out of blob storage - // +kubebuilder:default:=true - // +kubebuilder:validation:Optional - PrefetchData bool `json:"prefetchData"` -} - -// Options configures optional behaviors of the operator, like ingress, casing, and data prefetching. -// +kubebuilder:validation:Type=object -type Options struct { - BaseOptions `json:",inline"` - WMSOptions `json:",inline"` -} - -func GetDefaultOptions() *Options { - return &Options{ - BaseOptions: BaseOptions{ - IncludeIngress: true, - AutomaticCasing: true, - PrefetchData: true, - }, - WMSOptions: WMSOptions{ - ValidateRequests: true, - RewriteGroupToDataLayers: false, - DisableWebserviceProxy: false, - ValidateChildStyleNameEqual: false, - }, - } -} - -// BaseService holds all shared Services field for all apis -type BaseService struct { - // Geonovum subdomein - // +kubebuilder:validation:MinLength:=1 - Prefix string `json:"prefix"` - - // URL of the service - URL smoothoperatormodel.URL `json:"url"` - - // External Mapfile reference - Mapfile *Mapfile `json:"mapfile,omitempty"` - - // Reference to OwnerInfo CR - // +kubebuilder:validation:MinLength:=1 - OwnerInfoRef string `json:"ownerInfoRef"` - - // Service title - // +kubebuilder:validation:MinLength:=1 - Title string `json:"title"` - - // Service abstract - // +kubebuilder:validation:MinLength:=1 - Abstract string `json:"abstract"` - - // Keywords for capabilities - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:items:MinLength:=1 - Keywords []string `json:"keywords"` - - // Optional Fees - // +kubebuilder:validation:MinLength:=1 - Fees *string `json:"fees,omitempty"` - - // AccessConstraints URL - // +kubebuilder:default="https://creativecommons.org/publicdomain/zero/1.0/deed.nl" - AccessConstraints smoothoperatormodel.URL `json:"accessConstraints,omitempty"` -} - -// Inspire holds INSPIRE-specific metadata for the service. -// +kubebuilder:validation:Type=object -type Inspire struct { - // ServiceMetadataURL references the CSW or custom metadata record. - // +kubebuilder:validation:Type=object - ServiceMetadataURL MetadataURL `json:"serviceMetadataUrl"` - - // Language of the INSPIRE metadata record - // +kubebuilder:validation:Pattern:=`bul|cze|dan|dut|eng|est|fin|fre|ger|gre|hun|gle|ita|lav|lit|mlt|pol|por|rum|slo|slv|spa|swe` - Language string `json:"language"` -} - -// +kubebuilder:validation:XValidation:rule="(has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom))", message="metadataUrl should have exactly 1 of csw or custom" -type MetadataURL struct { - // CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. - CSW *Metadata `json:"csw,omitempty"` - - // Custom allows arbitrary href - Custom *Custom `json:"custom,omitempty"` -} - -// Metadata holds the UUID of a CSW metadata record -type Metadata struct { - // MetadataIdentifier is the record's UUID - // +kubebuilder:validation:Pattern:=`^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$` - MetadataIdentifier string `json:"metadataIdentifier"` -} - -// Custom represents a non-CSW metadata link with a href and MIME type. -// +kubebuilder:validation:Type=object -type Custom struct { - // Href of the custom metadata url - Href smoothoperatormodel.URL `json:"href"` - - // MIME type of the custom link - // +kubebuilder:validation:MinLength=1 - Type string `json:"type"` -} - -// BaseData holds the data source configuration for gpkg and postgis -type BaseData struct { - // Gpkg configures a GeoPackage file source - Gpkg *Gpkg `json:"gpkg,omitempty"` - - // Postgis configures a Postgis table source - Postgis *Postgis `json:"postgis,omitempty"` -} - -// Data holds the data source configuration -// +kubebuilder:validation:XValidation:rule="has(self.gpkg) || has(self.tif) || has(self.postgis)", message="Atleast one of the datasource should be provided (postgis, gpkg, tif)" -type Data struct { - BaseData `json:",inline"` - - // TIF configures a GeoTIF raster source - TIF *TIF `json:"tif,omitempty"` -} - -// Gpkg configures a Geopackage data source -// +kubebuilder:validation:Type=object -type Gpkg struct { - // Blobkey identifies the location/bucket of the .gpkg file - // +kubebuilder:validation:Pattern:=^.+\/.+\/.+\.gpkg$ - BlobKey string `json:"blobKey"` - - // TableName is the table within the geopackage - // +kubebuilder:validation:MinLength:=1 - TableName string `json:"tableName"` - - // GeometryType of the table, must match an OGC type - // +kubebuilder:validation:Pattern:=`^(Multi)?(Point|LineString|Polygon)$` - GeometryType string `json:"geometryType"` - - // Columns to visualize for this table - // +kubebuilder:validation:MinItems:=1 - Columns []Column `json:"columns"` -} - -// Postgis - reference to table in a Postgres database -// +kubebuilder:validation:Type=object -type Postgis struct { - // TableName in postGIS - // +kubebuilder:validation:MinLength=1 - TableName string `json:"tableName"` - - // GeometryType of the table - // +kubebuilder:validation:Pattern=`^(Multi)?(Point|LineString|Polygon)$` - GeometryType string `json:"geometryType"` - - // Columns to expose from table - // +kubebuilder:validation:MinItems=1 - Columns []Column `json:"columns"` -} - -// TIF configures a GeoTIFF raster data source -// +kubebuilder:validation:Type=object -type TIF struct { - // BlobKey to the TIFF file - // +kubebuilder:validation:Pattern:=`^.+\/.+\/.+\.(tif?f|vrt)$` - BlobKey string `json:"blobKey"` - - // This option can be used to control the resampling kernel used sampling raster images, optional - // +kubebuilder:validation:Pattern=`(NEAREST|AVERAGE|BILINEAR)` - // +kubebuilder:default=NEAREST - Resample string `json:"resample,omitempty"` - - // Controls the smoothing of the image on a certain point. Bigger value gives a smoother/better picture but - // results in slower web responses, optional - // +kubebuilder:validation:Pattern="^-?[0-9]+([.][0-9]*)?$" - // +kubebuilder:default="2.5" - OversampleRatio string `json:"oversampleRatio,omitempty"` - - // Sets the color index to treat as transparent for raster layers, optional, hex or rgb - // +kubebuilder:validation:Pattern=`(#[0-9A-F]{6}([0-9A-F]{2})?)|([0-9]{1,3}\s[0-9]{1,3}\s[0-9]{1,3})` - Offsite *string `json:"offsite,omitempty"` - - // "When a band represents nominal or ordinal data the class name (from styling) can be included in the getFeatureInfo" - // +kubebuilder:default:=false - GetFeatureInfoIncludesClass bool `json:"getFeatureInfoIncludesClass,omitempty"` -} - -// Column maps a source column name to an optional alias for output. -// +kubebuilder:validation:Type=object -type Column struct { - // Name of the column in the data source. - // +kubebuilder:validation:MinLength=1 - Name string `json:"name"` - - // Alias for the column in the service output. - // +kubebuilder:validation:MinLength=1 - Alias *string `json:"alias,omitempty"` -} - -func SetHost(url string) { - host = strings.TrimSuffix(url, "/") -} - -func GetHost(includeProtocol bool) string { - if !includeProtocol && strings.HasPrefix(host, "http") { - return strings.Split(host, "://")[1] - } - - return host -} - -func (d *BaseData) GetColumns() *[]Column { - switch { - case d.Gpkg != nil: - return &d.Gpkg.Columns - case d.Postgis != nil: - return &d.Postgis.Columns - default: - return nil - } -} - -func (d *BaseData) GetTableName() *string { - switch { - case d.Gpkg != nil: - return &d.Gpkg.TableName - case d.Postgis != nil: - return &d.Postgis.TableName - default: - return nil - } -} - -func (d *BaseData) GetGeometryType() *string { - switch { - case d.Gpkg != nil: - return &d.Gpkg.GeometryType - case d.Postgis != nil: - return &d.Postgis.GeometryType - default: - return nil - } -} - -func (o Options) UseWebserviceProxy() bool { - // options.DisableWebserviceProxy not set or false - return !o.DisableWebserviceProxy -} diff --git a/api/v3/shared_validation.go b/api/v3/shared_validation.go deleted file mode 100644 index bd87685..0000000 --- a/api/v3/shared_validation.go +++ /dev/null @@ -1,210 +0,0 @@ -package v3 - -import ( - "context" - "fmt" - "slices" - - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - sharedValidation "github.com/pdok/smooth-operator/pkg/validation" - v1 "k8s.io/api/core/v1" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -func ValidateCreate[W WMSWFS](c client.Client, obj W, validate func(W, *[]string, *field.ErrorList)) ([]string, error) { - warnings := []string{} - allErrs := field.ErrorList{} - - err := sharedValidation.ValidateLabelsOnCreate(obj.GetLabels()) - if err != nil { - allErrs = append(allErrs, err) - } - - err = sharedValidation.ValidateIngressRouteURLsContainsBaseURL(obj.IngressRouteURLs(false), obj.URL(), nil) - if err != nil { - allErrs = append(allErrs, err) - } - - validate(obj, &warnings, &allErrs) - ValidateOwnerInfo(c, obj, &allErrs) - - if len(allErrs) == 0 { - return warnings, nil - } - - return warnings, apierrors.NewInvalid( - obj.GroupKind(), - obj.GetName(), allErrs) -} - -func ValidateUpdate[W WMSWFS](c client.Client, newW, oldW W, validate func(W, *[]string, *field.ErrorList)) ([]string, error) { - warnings := []string{} - allErrs := field.ErrorList{} - - // Make sure no ingressRouteURLs have been removed - sharedValidation.ValidateIngressRouteURLsNotRemoved(oldW.IngressRouteURLs(false), newW.IngressRouteURLs(true), &allErrs, nil) - - if len(newW.IngressRouteURLs(false)) == 0 { - // There are no ingressRouteURLs given, spec.service.url is immutable is that case. - path := field.NewPath("spec").Child("service").Child("url") - sharedValidation.CheckURLImmutability( - oldW.URL(), - newW.URL(), - &allErrs, - path, - ) - } else if oldW.URL().String() != newW.URL().String() { - // Make sure both the old spec.service.url and the new one are included in the ingressRouteURLs list. - err := sharedValidation.ValidateIngressRouteURLsContainsBaseURL(newW.IngressRouteURLs(true), oldW.URL(), nil) - if err != nil { - allErrs = append(allErrs, err) - } - - err = sharedValidation.ValidateIngressRouteURLsContainsBaseURL(newW.IngressRouteURLs(true), newW.URL(), nil) - if err != nil { - allErrs = append(allErrs, err) - } - } - - sharedValidation.ValidateLabelsOnUpdate(oldW.GetLabels(), newW.GetLabels(), &allErrs) - - if (newW.Inspire() == nil && oldW.Inspire() != nil) || (newW.Inspire() != nil && oldW.Inspire() == nil) { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("service").Child("inspire"), "cannot change from inspire to not inspire or the other way around")) - } - - validate(newW, &warnings, &allErrs) - ValidateOwnerInfo(c, newW, &allErrs) - - if len(allErrs) == 0 { - return warnings, nil - } - return warnings, apierrors.NewInvalid( - newW.GroupKind(), - newW.GetName(), allErrs) -} - -func ValidateHorizontalPodAutoscalerPatch(patch HorizontalPodAutoscalerPatch, allErrs *field.ErrorList) { - path := field.NewPath("spec").Child("horizontalPodAutoscaler") - // TODO: replace hardcoded defaults with dynamic defaults from cli options or ownerInfo - var minReplicas, maxReplicas int32 = 2, 32 - if patch.MinReplicas != nil { - minReplicas = *patch.MinReplicas - } - if patch.MaxReplicas != nil { - maxReplicas = *patch.MaxReplicas - } - - if maxReplicas < minReplicas { - replicas := fmt.Sprintf("minReplicas: %d, maxReplicas: %d", minReplicas, maxReplicas) - - *allErrs = append(*allErrs, field.Invalid(path, replicas, "maxReplicas cannot be less than minReplicas")) - } - -} - -func ValidateEphemeralStorage(podSpecPatch v1.PodSpec, allErrs *field.ErrorList) { - path := field.NewPath("spec"). - Child("podSpecPatch"). - Child("containers"). - Key("mapserver"). - Child("resources"). - Child("limits"). - Child(v1.ResourceEphemeralStorage.String()) - storageSet := false - for _, container := range podSpecPatch.Containers { - if container.Name == "mapserver" { - _, storageSet = container.Resources.Limits[v1.ResourceEphemeralStorage] - } - } - if !storageSet { - *allErrs = append(*allErrs, field.Required(path, "")) - } -} - -func ValidateInspire[O WMSWFS](obj O, allErrs *field.ErrorList, allWarnings *[]string) { - if obj.Inspire() == nil { - return - } - - datasetIDs := obj.DatasetMetadataIDs() - spatialID := obj.Inspire().SpatialDatasetIdentifier - - if slices.Contains(datasetIDs, spatialID) { - *allWarnings = append(*allWarnings, field.Invalid( - field.NewPath("spec").Child("service").Child("inspire").Child("spatialDatasetIdentifier"), - spatialID, - "spatialDatasetIdentifier should not also be used as an datasetMetadataUrl.csw.metadataIdentifier", - ).Error()) - } - - if serviceID := obj.Inspire().ServiceMetadataURL.CSW; serviceID != nil { - if slices.Contains(datasetIDs, serviceID.MetadataIdentifier) { - *allErrs = append(*allErrs, field.Invalid( - field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), - serviceID.MetadataIdentifier, - "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as an datasetMetadataUrl.csw.metadataIdentifier", - )) - } - - if spatialID == serviceID.MetadataIdentifier { - *allErrs = append(*allErrs, field.Invalid( - field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), - serviceID.MetadataIdentifier, - "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as the spatialDatasetIdentifier", - )) - } - } - - if obj.Type() == ServiceTypeWFS && len(datasetIDs) > 1 { - *allErrs = append(*allErrs, field.Invalid( - field.NewPath("spec").Child("service").Child("featureTypes[*]").Child("datasetMetadataUrl").Child("csw").Child("metadataIdentifier"), - datasetIDs, - "when Inspire, all featureTypes need use the same datasetMetadataUrl.csw.metadataIdentifier", - )) - } - -} - -func ValidateOwnerInfo[O WMSWFS](c client.Client, obj O, allErrs *field.ErrorList) { - ownerInfoRef := obj.OwnerInfoRef() - ownerInfo := &smoothoperatorv1.OwnerInfo{} - objectKey := client.ObjectKey{ - Namespace: obj.GetNamespace(), - Name: ownerInfoRef, - } - ctx := context.Background() - err := c.Get(ctx, objectKey, ownerInfo) - fieldPath := field.NewPath("spec").Child("service").Child("ownerInfoRef") - if err != nil { - *allErrs = append(*allErrs, field.NotFound(fieldPath, ownerInfoRef)) - return - } - - if ownerInfo.Spec.NamespaceTemplate == nil { - *allErrs = append(*allErrs, field.Required(fieldPath, "spec.namespaceTemplate missing in "+ownerInfo.Name)) - return - } - - if ((obj.Inspire() != nil && obj.Inspire().ServiceMetadataURL.CSW != nil) || - len(obj.DatasetMetadataIDs()) > 0) && - (ownerInfo.Spec.MetadataUrls == nil || ownerInfo.Spec.MetadataUrls.CSW == nil) { - *allErrs = append(*allErrs, field.Required(fieldPath, "spec.metadataUrls.csw missing in "+ownerInfo.Name)) - return - } - - switch obj.Type() { - case ServiceTypeWFS: - if ownerInfo.Spec.WFS == nil { - *allErrs = append(*allErrs, field.Required(fieldPath, "spec.WFS missing in "+ownerInfo.Name)) - } - case ServiceTypeWMS: - if ownerInfo.Spec.WMS == nil { - *allErrs = append(*allErrs, field.Required(fieldPath, "spec.WMS missing in "+ownerInfo.Name)) - } - } - -} diff --git a/api/v3/wfs_conversion.go b/api/v3/wfs_conversion.go index 372bdb3..4e46996 100644 --- a/api/v3/wfs_conversion.go +++ b/api/v3/wfs_conversion.go @@ -1,25 +1,17 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v3 diff --git a/api/v3/wfs_types.go b/api/v3/wfs_types.go index 3a67839..85e8121 100644 --- a/api/v3/wfs_types.go +++ b/api/v3/wfs_types.go @@ -1,66 +1,55 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v3 import ( - "errors" - "slices" - "strings" - - smoothoperatormodel "github.com/pdok/smooth-operator/model" - - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// WFSSpec defines the desired state of WFS. +type WFSSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of WFS. Edit wfs_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// WFSStatus defines the observed state of WFS. +type WFSStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:conversion:hub // +kubebuilder:subresource:status -// versionName=v3 -// +kubebuilder:resource:path=wfs -// +kubebuilder:resource:categories=pdok -// +kubebuilder:printcolumn:name="ReadyPods",type=integer,JSONPath=`.status.podSummary[0].ready` -// +kubebuilder:printcolumn:name="DesiredPods",type=integer,JSONPath=`.status.podSummary[0].total` -// +kubebuilder:printcolumn:name="ReconcileStatus",type=string,JSONPath=`.status.conditions[?(@.type == "Reconciled")].reason` // WFS is the Schema for the wfs API. type WFS struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec WFSSpec `json:"spec"` - Status smoothoperatormodel.OperatorStatus `json:"status,omitempty"` -} - -func (wfs *WFS) OperatorStatus() *smoothoperatormodel.OperatorStatus { - return &wfs.Status + Spec WFSSpec `json:"spec,omitempty"` + Status WFSStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true @@ -75,247 +64,3 @@ type WFSList struct { func init() { SchemeBuilder.Register(&WFS{}, &WFSList{}) } - -// WFSSpec vertegenwoordigt de hoofdstruct voor de YAML-configuratie -// +kubebuilder:validation:XValidation:rule="!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)",messageExpression="'ingressRouteUrls should include service.url '+self.service.url" -type WFSSpec struct { - // Optional lifecycle settings - Lifecycle *smoothoperatormodel.Lifecycle `json:"lifecycle,omitempty"` - - // +kubebuilder:validation:Type=object - // +kubebuilder:validation:Schemaless - // +kubebuilder:pruning:PreserveUnknownFields - // Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. - PodSpecPatch corev1.PodSpec `json:"podSpecPatch"` - HorizontalPodAutoscalerPatch *HorizontalPodAutoscalerPatch `json:"horizontalPodAutoscalerPatch,omitempty"` - // TODO omitting the options field or setting an empty value results in incorrect defaulting of the options - // Options configures optional behaviors of the operator, like ingress, casing, and data prefetching. - Options *BaseOptions `json:"options,omitempty"` - - // Custom healthcheck options - HealthCheck *HealthCheckWFS `json:"healthCheck,omitempty"` - - // Optional list of URLs where the service can be reached - // By default only the spec.service.url is used - IngressRouteURLs smoothoperatormodel.IngressRouteURLs `json:"ingressRouteUrls,omitempty"` - - // service configuration - Service WFSService `json:"service"` -} - -// +kubebuilder:validation:XValidation:message="otherCrs can't contain the defaultCrs",rule="!has(self.otherCrs) || (has(self.otherCrs) && !(self.defaultCrs in self.otherCrs))",fieldPath=".otherCrs" -type WFSService struct { - BaseService `json:",inline"` - - // Inspire holds INSPIRE-specific metadata for the service. - Inspire *WFSInspire `json:"inspire,omitempty"` - - // Default CRS (DataEPSG) - // +kubebuilder:validation:Pattern:="^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$" - DefaultCrs string `json:"defaultCrs"` - - // Other supported CRS - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:items:Pattern:="^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$" - OtherCrs []string `json:"otherCrs,omitempty"` - - // Service bounding box - Bbox *Bbox `json:"bbox,omitempty"` - - // CountDefault -> wfs_maxfeatures in mapfile - // +kubebuilder:validation:Minimum:=1 - CountDefault *int `json:"countDefault,omitempty"` - - // FeatureTypes configurations - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:Type=array - FeatureTypes []FeatureType `json:"featureTypes"` -} - -func (s WFSService) KeywordsIncludingInspireKeyword() []string { - keywords := s.Keywords - if s.Inspire != nil && !slices.Contains(keywords, "infoFeatureAccessService") { - keywords = append(keywords, "infoFeatureAccessService") - } - - return keywords -} - -// HealthCheck is the struct with all fields to configure custom healthchecks -type HealthCheckWFS struct { - // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('service=wfs')",message="a valid healthcheck contains 'Service=WFS'" - // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('request=')",message="a valid healthcheck contains 'Request='" - Querystring string `json:"querystring"` - // +kubebuilder:validation:Pattern=(image/png|text/xml|text/html) - Mimetype string `json:"mimetype"` -} - -type WFSInspire struct { - Inspire `json:",inline"` - // SpatialDatasetIdentifier is the ID uniquely identifying the dataset. - // +kubebuilder:validation:Pattern:=`^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$` - SpatialDatasetIdentifier string `json:"spatialDatasetIdentifier"` -} - -type Bbox struct { - // EXTENT/wfs_extent in mapfile - //nolint:tagliatelle - // +kubebuilder:validation:Type=object - DefaultCRS smoothoperatormodel.BBox `json:"defaultCRS"` -} - -// FeatureType defines a WFS feature -type FeatureType struct { - // Name of the feature - // +kubebuilder:validation:Pattern:=`^\S+$` - Name string `json:"name"` - - // Title of the feature - // +kubebuilder:validation:MinLength:=1 - Title string `json:"title"` - - // Abstract of the feature - // +kubebuilder:validation:MinLength:=1 - Abstract string `json:"abstract"` - - // Keywords of the feature - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:items:MinLength:=1 - Keywords []string `json:"keywords"` - - // Metadata URL - // +kubebuilder:validation:Type=object - DatasetMetadataURL *MetadataURL `json:"datasetMetadataUrl,omitempty"` - - // Optional feature bbox - // +kubebuilder:validation:Optional - // +kubebuilder:validation:Type:=object - Bbox *FeatureBbox `json:"bbox,omitempty"` - - // FeatureType data connection - // +kubebuilder:validation:Type=object - // +kubebuilder:validation:XValidation:rule="has(self.gpkg) || has(self.postgis)", message="At least one of the datasource should be provided (postgis, gpkg)" - Data BaseData `json:"data"` -} - -// FeatureBbox is the optional featureType bounding box, if provided it overrides the default extent -type FeatureBbox struct { - // DefaultCRS defines the EXTENT/wfs_extent for the featureType for use in the mapfile - //nolint:tagliatelle - // +kubebuilder:validation:Type=object - DefaultCRS *smoothoperatormodel.BBox `json:"defaultCRS,omitempty"` - - // WGS84, if provided, gives the same bounding box reprojected into EPSG:4326 for use in the capabilities. - // +kubebuilder:validation:Type=object - WGS84 *smoothoperatormodel.BBox `json:"wgs84,omitempty"` -} - -func (wfs *WFS) HasPostgisData() bool { - for _, featureType := range wfs.Spec.Service.FeatureTypes { - if featureType.Data.Postgis != nil { - return true - } - } - return false -} - -func (wfs *WFS) GroupKind() schema.GroupKind { - return schema.GroupKind{Group: GroupVersion.Group, Kind: wfs.Kind} -} - -func (wfs *WFS) Inspire() *WFSInspire { - return wfs.Spec.Service.Inspire -} - -func (wfs *WFS) Mapfile() *Mapfile { - return wfs.Spec.Service.Mapfile -} - -func (wfs *WFS) Type() ServiceType { - return ServiceTypeWFS -} - -func (wfs *WFS) TypedName() string { - name := wfs.GetName() - typeSuffix := strings.ToLower(string(ServiceTypeWFS)) - - if strings.HasSuffix(name, typeSuffix) { - return name - } - - return name + "-" + typeSuffix -} - -func (wfs *WFS) PodSpecPatch() corev1.PodSpec { - return wfs.Spec.PodSpecPatch -} - -func (wfs *WFS) HorizontalPodAutoscalerPatch() *HorizontalPodAutoscalerPatch { - return wfs.Spec.HorizontalPodAutoscalerPatch -} - -func (wfs *WFS) Options() Options { - if wfs.Spec.Options == nil { - return *GetDefaultOptions() - } - - return Options{BaseOptions: *wfs.Spec.Options} -} - -func (wfs *WFS) URL() smoothoperatormodel.URL { - return wfs.Spec.Service.URL -} - -func (wfs *WFS) DatasetMetadataIDs() []string { - ids := []string{} - - for _, featureType := range wfs.Spec.Service.FeatureTypes { - if featureType.DatasetMetadataURL != nil && featureType.DatasetMetadataURL.CSW != nil { - if id := featureType.DatasetMetadataURL.CSW.MetadataIdentifier; !slices.Contains(ids, id) { - ids = append(ids, id) - } - } - } - - return ids -} - -func (wfs *WFS) GeoPackages() []*Gpkg { - gpkgs := make([]*Gpkg, 0) - - for _, ft := range wfs.Spec.Service.FeatureTypes { - if ft.Data.Gpkg != nil { - gpkgs = append(gpkgs, ft.Data.Gpkg) - } - } - - return gpkgs -} - -func (wfs *WFS) ReadinessQueryString() (string, string, error) { - if hc := wfs.Spec.HealthCheck; hc != nil { - return hc.Querystring, hc.Mimetype, nil - } - - if len(wfs.Spec.Service.FeatureTypes) == 0 { - return "", "", errors.New("cannot get readiness probe for WFS, featuretypes could not be found") - } - - return "SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=" + wfs.Spec.Service.FeatureTypes[0].Name + "&STARTINDEX=0&COUNT=1", "text/xml", nil -} - -func (wfs *WFS) IngressRouteURLs(includeServiceURLWhenEmpty bool) smoothoperatormodel.IngressRouteURLs { - if len(wfs.Spec.IngressRouteURLs) == 0 { - if includeServiceURLWhenEmpty { - return smoothoperatormodel.IngressRouteURLs{{URL: wfs.Spec.Service.URL}} - } - - return smoothoperatormodel.IngressRouteURLs{} - } - - return wfs.Spec.IngressRouteURLs -} - -func (wfs *WFS) OwnerInfoRef() string { - return wfs.Spec.Service.OwnerInfoRef -} diff --git a/api/v3/wfs_validation.go b/api/v3/wfs_validation.go deleted file mode 100644 index 62eee7a..0000000 --- a/api/v3/wfs_validation.go +++ /dev/null @@ -1,101 +0,0 @@ -package v3 - -import ( - "slices" - "strings" - - "sigs.k8s.io/controller-runtime/pkg/client" - - sharedValidation "github.com/pdok/smooth-operator/pkg/validation" - - "k8s.io/apimachinery/pkg/util/validation/field" -) - -func (wfs *WFS) ValidateCreate(c client.Client) ([]string, error) { - return ValidateCreate(c, wfs, ValidateWFS) -} - -func (wfs *WFS) ValidateUpdate(c client.Client, wfsOld *WFS) ([]string, error) { - return ValidateUpdate(c, wfs, wfsOld, ValidateWFS) -} - -func ValidateWFS(wfs *WFS, warnings *[]string, allErrs *field.ErrorList) { - if strings.Contains(wfs.GetName(), "wfs") { - sharedValidation.AddWarning( - warnings, - *field.NewPath("metadata").Child("name"), - "name should not contain wfs", - wfs.GroupVersionKind(), - wfs.GetName(), - ) - } - - service := wfs.Spec.Service - path := field.NewPath("spec").Child("service") - - if service.Mapfile == nil && service.DefaultCrs != "EPSG:28992" && service.Bbox == nil { - *allErrs = append(*allErrs, field.Required( - path.Child("bbox").Child("defaultCRS"), - "when service.defaultCRS is not 'EPSG:28992'", - )) - } - - if service.Mapfile != nil && service.Bbox != nil { - sharedValidation.AddWarning( - warnings, - *path.Child("bbox"), - "is not used when service.mapfile is configured", - wfs.GroupVersionKind(), - wfs.GetName(), - ) - } - - crsses := []string{} - for i, crs := range service.OtherCrs { - if slices.Contains(crsses, crs) { - *allErrs = append(*allErrs, field.Duplicate( - path.Child("otherCrs").Index(i), - crs, - )) - } else { - crsses = append(crsses, crs) - } - } - - ValidateInspire(wfs, allErrs, warnings) - - if wfs.Spec.HorizontalPodAutoscalerPatch != nil { - ValidateHorizontalPodAutoscalerPatch(*wfs.Spec.HorizontalPodAutoscalerPatch, allErrs) - } - - podSpecPatch := wfs.Spec.PodSpecPatch - ValidateEphemeralStorage(podSpecPatch, allErrs) - - ValidateFeatureTypes(wfs, warnings, allErrs) -} - -func ValidateFeatureTypes(wfs *WFS, warnings *[]string, allErrs *field.ErrorList) { - names := []string{} - path := field.NewPath("spec").Child("service").Child("featureTypes") - for index, featureType := range wfs.Spec.Service.FeatureTypes { - if slices.Contains(names, featureType.Name) { - *allErrs = append(*allErrs, field.Duplicate( - path.Index(index).Child("name"), - featureType.Name, - )) - } else { - names = append(names, featureType.Name) - } - - if wfs.Spec.Service.Mapfile != nil && featureType.Bbox != nil && featureType.Bbox.DefaultCRS != nil { - sharedValidation.AddWarning( - warnings, - *path.Index(index).Child("bbox").Child("defaultCrs"), - "is not used when service.mapfile is configured", - wfs.GroupVersionKind(), - wfs.GetName(), - ) - } - - } -} diff --git a/api/v3/wms_conversion.go b/api/v3/wms_conversion.go index c262d39..2ff6ed1 100644 --- a/api/v3/wms_conversion.go +++ b/api/v3/wms_conversion.go @@ -1,25 +1,17 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v3 diff --git a/api/v3/wms_types.go b/api/v3/wms_types.go index e0d9f79..c00588e 100644 --- a/api/v3/wms_types.go +++ b/api/v3/wms_types.go @@ -1,75 +1,55 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v3 import ( - "errors" - "fmt" - "maps" - "slices" - "sort" - "strings" - - smoothoperatormodel "github.com/pdok/smooth-operator/model" - - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -const ( - TopLayer = "topLayer" - DataLayer = "dataLayer" - GroupLayer = "groupLayer" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// WMSSpec defines the desired state of WMS. +type WMSSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of WMS. Edit wms_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// WMSStatus defines the observed state of WMS. +type WMSStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:conversion:hub // +kubebuilder:subresource:status -// versionName=v3 -// +kubebuilder:resource:path=wms -// +kubebuilder:resource:categories=pdok -// +kubebuilder:printcolumn:name="ReadyPods",type=integer,JSONPath=`.status.podSummary[0].ready` -// +kubebuilder:printcolumn:name="DesiredPods",type=integer,JSONPath=`.status.podSummary[0].total` -// +kubebuilder:printcolumn:name="ReconcileStatus",type=string,JSONPath=`.status.conditions[?(@.type == "Reconciled")].reason` // WMS is the Schema for the wms API. type WMS struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec WMSSpec `json:"spec"` - Status smoothoperatormodel.OperatorStatus `json:"status,omitempty"` -} - -func (wms *WMS) OperatorStatus() *smoothoperatormodel.OperatorStatus { - return &wms.Status + Spec WMSSpec `json:"spec,omitempty"` + Status WMSStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true @@ -84,626 +64,3 @@ type WMSList struct { func init() { SchemeBuilder.Register(&WMS{}, &WMSList{}) } - -// WMSSpec defines the desired state of WMS. -// +kubebuilder:validation:XValidation:rule="!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)",messageExpression="'ingressRouteUrls should include service.url '+self.service.url" -type WMSSpec struct { - // Optional lifecycle settings - Lifecycle *smoothoperatormodel.Lifecycle `json:"lifecycle,omitempty"` - - // +kubebuilder:validation:Type=object - // +kubebuilder:validation:Schemaless - // +kubebuilder:pruning:PreserveUnknownFields - // Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. - PodSpecPatch corev1.PodSpec `json:"podSpecPatch"` - - // Optional specification for the HorizontalAutoscaler - HorizontalPodAutoscalerPatch *HorizontalPodAutoscalerPatch `json:"horizontalPodAutoscalerPatch,omitempty"` - - // Optional options for the configuration of the service. - // TODO omitting the options field or setting an empty value results in incorrect defaulting of the options - Options *Options `json:"options,omitempty"` - - // Custom healthcheck options - HealthCheck *HealthCheckWMS `json:"healthCheck,omitempty"` - - // Optional list of URLs where the service can be reached - // By default only the spec.service.url is used - IngressRouteURLs smoothoperatormodel.IngressRouteURLs `json:"ingressRouteUrls,omitempty"` - - // Service specification - Service WMSService `json:"service"` -} - -// +kubebuilder:validation:XValidation:message="service requires styling, either through service.mapfile, or stylingAssets.configMapRefs",rule=has(self.mapfile) || (has(self.stylingAssets) && has(self.stylingAssets.configMapRefs)) -// +kubebuilder:validation:XValidation:message="when using service.mapfile, don't include stylingAssets.configMapRefs",rule=!has(self.mapfile) || (!has(self.stylingAssets) || !has(self.stylingAssets.configMapRefs)) -type WMSService struct { - BaseService `json:",inline"` - - // Config for Inspire services - Inspire *Inspire `json:"inspire,omitempty"` - - // CRS of the data - // +kubebuilder:validation:Pattern:=`(EPSG|CRS):\d+` - //nolint:tagliatelle - DataEPSG string `json:"dataEPSG"` - - // Mapfile setting: Sets the maximum size (in pixels) for both dimensions of the image from a getMap request. - // +kubebuilder:validation:Minimum:=1 - MaxSize *int32 `json:"maxSize,omitempty"` - - // Mapfile setting: Sets the RESOLUTION field in the mapfile, not used when service.mapfile is configured - Resolution *int32 `json:"resolution,omitempty"` - - // Mapfile setting: Sets the DEFRESOLUTION field in the mapfile, not used when service.mapfile is configured - DefResolution *int32 `json:"defResolution,omitempty"` - - // Optional. Required files for the styling of the service - StylingAssets *StylingAssets `json:"stylingAssets,omitempty"` - - // Custom mapfile - Mapfile *Mapfile `json:"mapfile,omitempty"` - - // Toplayer - Layer Layer `json:"layer"` -} - -func (wmsService WMSService) KeywordsIncludingInspireKeyword() []string { - keywords := wmsService.Keywords - if wmsService.Inspire != nil && !slices.Contains(keywords, "infoMapAccessService") { - keywords = append(keywords, "infoMapAccessService") - } - - return keywords -} - -// HealthCheck is the struct with all fields to configure custom healthchecks -// +kubebuilder:validation:XValidation:rule="!has(self.querystring) || has(self.mimetype)",message="mimetype is required when a querystring is used" -// +kubebuilder:validation:XValidation:rule="(has(self.boundingbox) || has(self.querystring)) && !(has(self.querystring) && has(self.boundingbox))", message="healthcheck should have exactly 1 of querystring + mimetype or boundingbox" -// +kubebuilder:validation:XValidation:rule="(has(self.boundingbox) || has(self.mimetype)) && !(has(self.mimetype) && has(self.boundingbox))", message="healthcheck should have exactly 1 of querystring + mimetype or boundingbox" -type HealthCheckWMS struct { - // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('service=wms')",message="a valid healthcheck contains 'SERVICE=WMS'" - // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('request=')",message="a valid healthcheck contains 'REQUEST='" - Querystring *string `json:"querystring,omitempty"` - // +kubebuilder:validation:Pattern=(image/png|text/xml|text/html) - Mimetype *string `json:"mimetype,omitempty"` - - Boundingbox *smoothoperatormodel.BBox `json:"boundingbox,omitempty"` -} - -// StylingAssets contains the files references needed for styling -// +kubebuilder:validation:XValidation:message="At least one of blobKeys or configMapRefs is required",rule="has(self.blobKeys) || has(self.configMapRefs)" -type StylingAssets struct { - // BlobKeys contains symbol image (.png/.svg) or font (.ttf) keys on blob storage, format: container/key/file.(png|ttf) - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:items:Pattern:=^.+\/.+\/.+\.(png|ttf|svg)$ - BlobKeys []string `json:"blobKeys,omitempty"` - - // +kubebuilder:validation:MinItems:=1 - ConfigMapRefs []ConfigMapRef `json:"configMapRefs,omitempty"` -} - -type ConfigMapRef struct { - // Name is the name of the ConfigMap - // +kubebuilder:validation:MinLength:=1 - Name string `json:"name"` - - // Keys contains styling assets that contain mapfile code (.style|.symbol), required if you use symbols in your styles - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:items:Pattern:=^\S*.\.(style|symbol) - Keys []string `json:"keys,omitempty"` -} - -// +kubebuilder:validation:XValidation:message="A layer should have exactly one of sublayers or data", rule="(has(self.data) || has(self.layers)) && !(has(self.data) && has(self.layers))" -// +kubebuilder:validation:XValidation:message="A layer with data attribute should have styling", rule="!has(self.data) || has(self.styles)" -// +kubebuilder:validation:XValidation:message="A layer should have a title when visible", rule="!self.visible || has(self.title)" -// +kubebuilder:validation:XValidation:message="A layer should have an abstract when visible", rule="!self.visible || has(self.abstract)" -// +kubebuilder:validation:XValidation:message="A layer should have keywords when visible", rule="!self.visible || has(self.keywords)" -type Layer struct { - // Name of the layer, required for layers on the 2nd or 3rd level - // +kubebuilder:validation:MinLength:=1 - Name *string `json:"name,omitempty"` - - // Title of the layer - // +kubebuilder:validation:MinLength:=1 - Title *string `json:"title,omitempty"` - - // Abstract of the layer - // +kubebuilder:validation:MinLength:=1 - Abstract *string `json:"abstract,omitempty"` - - // Keywords of the layer, required if the layer is visible - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:items:MinLength:=1 - Keywords []string `json:"keywords,omitempty"` - - // BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. - // +kubebuilder:validation:MinItems:=1 - BoundingBoxes []WMSBoundingBox `json:"boundingBoxes,omitempty"` - - // Whether or not the layer is visible. At least one of the layers must be visible. - // +kubebuilder:default:=true - // +kubebuilder:validation:Optional - Visible bool `json:"visible"` - - // TODO ?? - Authority *Authority `json:"authority,omitempty"` - - // Links to metadata - DatasetMetadataURL *MetadataURL `json:"datasetMetadataUrl,omitempty"` - - // The minimum scale at which this layer functions - // +kubebuilder:validation:Pattern:=`^[0-9]+(.[0-9]+)?$` - MinScaleDenominator *string `json:"minscaledenominator,omitempty"` - - // The maximum scale at which this layer functions - // +kubebuilder:validation:Pattern:=`^[1-9][0-9]*(.[0-9]+)?$` - MaxScaleDenominator *string `json:"maxscaledenominator,omitempty"` - - // List of styles used by the layer - // +kubebuilder:validation:MinItems:=1 - Styles []Style `json:"styles,omitempty"` - - // Mapfile setting, sets "LABEL_NO_CLIP=ON" - LabelNoClip bool `json:"labelNoClip,omitempty"` - - // Data (gpkg/postgis/tif) used by the layer - Data *Data `json:"data,omitempty"` - - // Sublayers of the layer - // +kubebuilder:validation:MinItems:=1 - // +kubebuilder:validation:Type=array - Layers []Layer `json:"layers,omitempty"` -} - -type WMSBoundingBox struct { - // +kubebuilder:validation:Pattern:="^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$" - CRS string `json:"crs"` - BBox smoothoperatormodel.BBox `json:"bbox"` -} - -func (wmsBoundingBox *WMSBoundingBox) ToExtent() string { - return wmsBoundingBox.BBox.ToExtent() -} - -func (wmsBoundingBox *WMSBoundingBox) Combine(other *WMSBoundingBox) { - if wmsBoundingBox.CRS != other.CRS { - return - } - wmsBoundingBox.BBox.Combine(other.BBox) -} - -type Authority struct { - Name string `json:"name"` - URL string `json:"url"` - SpatialDatasetIdentifier string `json:"spatialDatasetIdentifier"` -} - -type Style struct { - // +kubebuilder:validation:MinLength:=1 - Name string `json:"name"` - - // +kubebuilder:validation:MinLength:=1 - Title *string `json:"title,omitempty"` - - // +kubebuilder:validation:MinLength:=1 - Abstract *string `json:"abstract,omitempty"` - - // +kubebuilder:validation:MinLength:=1 - Visualization *string `json:"visualization,omitempty"` - - Legend *Legend `json:"legend,omitempty"` -} - -type Legend struct { - // The width of the legend in px, defaults to 78 - // + kubebuilder:default=78 - Width int32 `json:"width,omitempty"` - - // The height of the legend in px, defaults to 20 - // + kubebuilder:default=20 - Height int32 `json:"height,omitempty"` - - // Format of the legend, defaults to image/png - // +kubebuilder:default="image/png" - Format string `json:"format,omitempty"` - - // Location of the legend on the blobstore - // +kubebuilder:validation:MinLength:=1 - BlobKey string `json:"blobKey"` -} - -// WMSOptions are the Options exclusively used by the WMS -// +kubebuilder:validation:Type=object -type WMSOptions struct { - - // ValidateRequests enables request validation against the service schema. - // +kubebuilder:default:=true - // +kubebuilder:validation:Optional - ValidateRequests bool `json:"validateRequests"` - - // RewriteGroupToDataLayers merges group layers into individual data layers. - // +kubebuilder:default:=false - // +kubebuilder:validation:Optional - RewriteGroupToDataLayers bool `json:"rewriteGroupToDataLayers"` - - // DisableWebserviceProxy disables the built-in proxy for external web services. - // +kubebuilder:default:=false - // +kubebuilder:validation:Optional - DisableWebserviceProxy bool `json:"disableWebserviceProxy"` - - // ValidateChildStyleNameEqual ensures child style names match the parent style. - // +kubebuilder:default=false - // +kubebuilder:validation:Optional - ValidateChildStyleNameEqual bool `json:"validateChildStyleNameEqual"` -} - -func (wmsService *WMSService) GetBoundingBox() WMSBoundingBox { - var boundingBox *WMSBoundingBox - - allLayers := wmsService.GetAnnotatedLayers() - for _, layer := range allLayers { - if len(layer.BoundingBoxes) > 0 { - for _, bbox := range wmsService.Layer.BoundingBoxes { - if boundingBox == nil { - boundingBox = &bbox - } else { - boundingBox.Combine(&bbox) - } - } - } - } - - if boundingBox != nil { - return *boundingBox - } - - return WMSBoundingBox{ - CRS: "EPSG:28992", - BBox: smoothoperatormodel.BBox{ - MinX: "-25000", - MaxX: "280000", - MinY: "250000", - MaxY: "860000", - }, - } -} - -func (stylingAssets *StylingAssets) GetAllConfigMapRefKeys() []string { - keys := []string{} - if stylingAssets != nil { - for _, cmRef := range stylingAssets.ConfigMapRefs { - keys = append(keys, cmRef.Keys...) - } - } - return keys -} - -type AnnotatedLayer struct { - // The name of the group that this layer belongs to, nil if it is not a member of a group. Groups can be a member of the toplayer as a group - GroupName *string - // Only for spec.Service.Layer - IsTopLayer bool - // Top layer or layer below the toplayer with children itself - IsGroupLayer bool - // Contains actual data - IsDataLayer bool - Layer -} - -func (wmsService *WMSService) GetAnnotatedLayers() []AnnotatedLayer { - result := make([]AnnotatedLayer, 0) - - result = append(result, AnnotatedLayer{ - GroupName: nil, - IsTopLayer: true, - IsGroupLayer: true, - IsDataLayer: false, - Layer: wmsService.Layer, - }) - - for _, middleLayer := range wmsService.Layer.Layers { - result = append(result, AnnotatedLayer{ - GroupName: wmsService.Layer.Name, - IsTopLayer: false, - IsGroupLayer: middleLayer.IsGroupLayer(), - IsDataLayer: middleLayer.IsDataLayer(), - Layer: middleLayer, - }) - - for _, bottomLayer := range middleLayer.Layers { - result = append(result, AnnotatedLayer{ - GroupName: middleLayer.Name, - IsTopLayer: false, - IsGroupLayer: false, - IsDataLayer: true, - Layer: bottomLayer, - }) - } - } - - return result -} - -// GetAllSublayers - get all sublayers of a layer, the result does not include the layer itself -func (layer *Layer) GetAllSublayers() []Layer { - layers := layer.Layers - for _, childLayer := range layer.Layers { - layers = append(layers, childLayer.GetAllSublayers()...) - } - return layers -} - -func (wmsService *WMSService) GetParentLayer(layer Layer) *Layer { - if wmsService.Layer.Layers == nil { - return nil - } - - for _, middleLayer := range wmsService.Layer.Layers { - if middleLayer.Name == layer.Name { - return &wmsService.Layer - } - - for _, bottomLayer := range middleLayer.Layers { - if bottomLayer.Name == layer.Name { - return &middleLayer - } - } - } - return nil -} - -func (layer *Layer) hasData() bool { - switch { - case layer.Data == nil: - return false - case layer.Data.Gpkg != nil: - return true - case layer.Data.Postgis != nil: - return true - case layer.Data.TIF != nil: - return true - default: - return false - } -} - -func (layer *Layer) hasTIFData() bool { - if !layer.hasData() { - return false - } - return layer.Data.TIF != nil && layer.Data.TIF.BlobKey != "" -} - -func (layer *Layer) IsDataLayer() bool { - return layer.hasData() && len(layer.Layers) == 0 -} - -func (layer *Layer) IsGroupLayer() bool { - return len(layer.Layers) > 0 -} - -// IsTopLayer - a layer is a toplayer if and only if it has sublayers that are group layers. -// In other words the layer is level 1 in a 3 level hierarchy. -func (layer *Layer) IsTopLayer() bool { - if layer.IsGroupLayer() { - for _, childLayer := range layer.Layers { - if childLayer.IsGroupLayer() { - return true - } - } - } - - return false -} - -func (layer *Layer) hasBoundingBoxForCRS(crs string) bool { - for _, bbox := range layer.BoundingBoxes { - if bbox.CRS == crs { - return true - } - } - return false -} - -func (layer *Layer) setInheritedBoundingBoxes() { - if len(layer.Layers) == 0 { - return - } - - var updatedLayers []Layer - for _, childLayer := range layer.Layers { - // Inherit parent boundingboxes - for _, boundingBox := range layer.BoundingBoxes { - if !childLayer.hasBoundingBoxForCRS(boundingBox.CRS) { - childLayer.BoundingBoxes = append(childLayer.BoundingBoxes, boundingBox) - } - } - childLayer.setInheritedBoundingBoxes() - updatedLayers = append(updatedLayers, childLayer) - } - layer.Layers = updatedLayers -} - -func (wms *WMS) GetAllLayersWithLegend() (layers []AnnotatedLayer) { - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if !layer.hasData() || len(layer.Styles) == 0 { - continue - } - for _, style := range layer.Styles { - if style.Legend != nil && style.Legend.BlobKey != "" { - layers = append(layers, layer) - break - } - } - } - return -} - -func (wms *WMS) GetUniqueTiffBlobKeys() []string { - blobKeys := map[string]bool{} - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if layer.hasTIFData() { - blobKeys[layer.Data.TIF.BlobKey] = true - } - } - keys := slices.Collect(maps.Keys(blobKeys)) - sort.Strings(keys) // This is only needed for the unit test - return keys -} - -func (wms *WMS) GetAuthority() *Authority { - if wms.Spec.Service.Layer.Authority != nil { - return wms.Spec.Service.Layer.Authority - } - - for _, childLayer := range wms.Spec.Service.Layer.Layers { - if childLayer.Authority != nil { - return childLayer.Authority - } else if childLayer.Layers != nil { - for _, grandChildLayer := range childLayer.Layers { - if grandChildLayer.Authority != nil { - return grandChildLayer.Authority - } - } - } - } - - return nil -} - -func (wms *WMS) HasPostgisData() bool { - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if layer.Data != nil && layer.Data.Postgis != nil { - return true - } - } - return false -} - -func (wms *WMS) GroupKind() schema.GroupKind { - return schema.GroupKind{Group: GroupVersion.Group, Kind: wms.Kind} -} - -func (wms *WMS) Inspire() *WFSInspire { - if wms.Spec.Service.Inspire != nil { - return &WFSInspire{Inspire: *wms.Spec.Service.Inspire} - } - return nil -} - -func (wms *WMS) Mapfile() *Mapfile { - return wms.Spec.Service.Mapfile -} - -func (wms *WMS) Type() ServiceType { - return ServiceTypeWMS -} - -func (wms *WMS) TypedName() string { - name := wms.GetName() - typeSuffix := strings.ToLower(string(ServiceTypeWMS)) - - if strings.HasSuffix(name, typeSuffix) { - return name - } - - return name + "-" + typeSuffix -} - -func (wms *WMS) PodSpecPatch() corev1.PodSpec { - return wms.Spec.PodSpecPatch -} - -func (wms *WMS) HorizontalPodAutoscalerPatch() *HorizontalPodAutoscalerPatch { - return wms.Spec.HorizontalPodAutoscalerPatch -} - -func (wms *WMS) Options() Options { - if wms.Spec.Options == nil { - return *GetDefaultOptions() - } - - return *wms.Spec.Options -} - -func (wms *WMS) URL() smoothoperatormodel.URL { - return wms.Spec.Service.URL -} - -func (wms *WMS) DatasetMetadataIDs() []string { - ids := []string{} - - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if layer.DatasetMetadataURL != nil && layer.DatasetMetadataURL.CSW != nil { - if id := layer.DatasetMetadataURL.CSW.MetadataIdentifier; !slices.Contains(ids, id) { - ids = append(ids, id) - } - } - } - - return ids -} - -func (wms *WMS) GeoPackages() []*Gpkg { - gpkgs := make([]*Gpkg, 0) - - for _, layer := range wms.Spec.Service.Layer.Layers { - if layer.Data != nil { - if layer.Data.Gpkg != nil { - gpkgs = append(gpkgs, layer.Data.Gpkg) - } - } else if layer.Layers != nil { - for _, childLayer := range layer.Layers { - if childLayer.Data != nil && childLayer.Data.Gpkg != nil { - gpkgs = append(gpkgs, childLayer.Data.Gpkg) - } - } - } - } - - return gpkgs -} - -func (wms *WMS) HealthCheckBBox() string { - if hc := wms.Spec.HealthCheck; hc != nil && hc.Boundingbox != nil { - return strings.ReplaceAll(hc.Boundingbox.ToExtent(), " ", ",") - } - - return "190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914" -} - -func (wms *WMS) ReadinessQueryString() (string, string, error) { - if hc := wms.Spec.HealthCheck; hc != nil && hc.Querystring != nil { - return *hc.Querystring, *hc.Mimetype, nil - } - - firstDataLayerName := "" - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if layer.IsDataLayer { - firstDataLayerName = *layer.Name - break - } - } - if firstDataLayerName == "" { - return "", "", errors.New("cannot get readiness probe for WMS, the first datalayer could not be found") - } - - return fmt.Sprintf("SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=%s&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=%s&STYLES=&FORMAT=image/png", wms.HealthCheckBBox(), firstDataLayerName), "image/png", nil -} - -func (wms *WMS) IngressRouteURLs(includeServiceURLWhenEmpty bool) smoothoperatormodel.IngressRouteURLs { - if len(wms.Spec.IngressRouteURLs) == 0 { - if includeServiceURLWhenEmpty { - return smoothoperatormodel.IngressRouteURLs{{URL: wms.Spec.Service.URL}} - } - - return smoothoperatormodel.IngressRouteURLs{} - } - - return wms.Spec.IngressRouteURLs -} - -func (wms *WMS) OwnerInfoRef() string { - return wms.Spec.Service.OwnerInfoRef -} diff --git a/api/v3/wms_types_test.go b/api/v3/wms_types_test.go deleted file mode 100644 index 9e328c6..0000000 --- a/api/v3/wms_types_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package v3 - -import ( - "reflect" - "testing" - - "github.com/google/go-cmp/cmp" - - smoothoperatormodel "github.com/pdok/smooth-operator/model" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -func TestLayer_setInheritedBoundingBoxes(t *testing.T) { - first28992BoundingBox := WMSBoundingBox{ - CRS: "EPSG:28992", - BBox: smoothoperatormodel.BBox{ - MinX: "482.06", - MaxX: "306602.42", - MinY: "284182.97", - MaxY: "637049.52", - }, - } - first4326BoundingBox := WMSBoundingBox{ - CRS: "EPSG:4326", - BBox: smoothoperatormodel.BBox{ - MinX: "2.35417303", - MaxX: "7.5553525", - MinY: "50.71447164", - MaxY: "55.66948102", - }, - } - first4258BoundingBox := WMSBoundingBox{ - CRS: "EPSG:4258", - BBox: smoothoperatormodel.BBox{ - MinX: "2.354173", - MaxX: "7.5553527", - MinY: "50.71447", - MaxY: "55.66948", - }} - second28992BoundingBox := WMSBoundingBox{ - CRS: "EPSG:28992", - BBox: smoothoperatormodel.BBox{ - MinX: "0.00", - MaxX: "310000.00", - MinY: "275000.00", - MaxY: "650000.00", - }} - - tests := []struct { - name string - layer Layer - toplayerExpectedBoundingBoxCount int - toplayerExpectedBoundingBoxes []WMSBoundingBox - grouplayer1ExpectedBoundingBoxCount int - grouplayer1ExpectedBoundingBoxes []WMSBoundingBox - datalayer1ExpectedBoundingBoxCount int - datalayer1ExpectedBoundingBoxes []WMSBoundingBox - datalayer2ExpectedBoundingBoxCount int - datalayer2ExpectedBoundingBoxes []WMSBoundingBox - }{ - { - name: "setInheritedBoundingBoxes for layer", - layer: Layer{ - Name: smoothoperatorutils.Pointer("toplayer"), - BoundingBoxes: []WMSBoundingBox{first28992BoundingBox}, - Layers: []Layer{ - { - Name: smoothoperatorutils.Pointer("grouplayer-1"), - BoundingBoxes: []WMSBoundingBox{first4326BoundingBox}, - Layers: []Layer{ - { - Name: smoothoperatorutils.Pointer("datalayer-1"), - BoundingBoxes: []WMSBoundingBox{first4258BoundingBox}, - }, - { - Name: smoothoperatorutils.Pointer("datalayer-2"), - BoundingBoxes: []WMSBoundingBox{second28992BoundingBox}, - }, - }, - }, - }, - }, - toplayerExpectedBoundingBoxCount: 1, - toplayerExpectedBoundingBoxes: []WMSBoundingBox{first28992BoundingBox}, - grouplayer1ExpectedBoundingBoxCount: 2, - grouplayer1ExpectedBoundingBoxes: []WMSBoundingBox{first4326BoundingBox, first28992BoundingBox}, - datalayer1ExpectedBoundingBoxCount: 3, - datalayer1ExpectedBoundingBoxes: []WMSBoundingBox{first4258BoundingBox, first4326BoundingBox, first28992BoundingBox}, - datalayer2ExpectedBoundingBoxCount: 2, - datalayer2ExpectedBoundingBoxes: []WMSBoundingBox{second28992BoundingBox, first4326BoundingBox}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - layer := tt.layer - layer.setInheritedBoundingBoxes() - - topChildLayers := layer.Layers - groupLayer1 := topChildLayers[0] - groupChildLayers := groupLayer1.Layers - dataLayer1 := groupChildLayers[0] - dataLayer2 := groupChildLayers[1] - - if len(layer.BoundingBoxes) != tt.toplayerExpectedBoundingBoxCount { - t.Errorf("Toplayer has unexpected number of bounding boxes = %v, want %v", len(layer.BoundingBoxes), tt.toplayerExpectedBoundingBoxCount) - } - if !cmp.Equal(layer.BoundingBoxes, tt.toplayerExpectedBoundingBoxes) { - t.Errorf("Toplayer has unexpected bounding boxes = %v, want %v", layer.BoundingBoxes, tt.toplayerExpectedBoundingBoxes) - } - if len(groupLayer1.BoundingBoxes) != tt.grouplayer1ExpectedBoundingBoxCount { - t.Errorf("Grouplayer has unexpected number of bounding boxes = %v, want %v", len(groupLayer1.BoundingBoxes), tt.grouplayer1ExpectedBoundingBoxCount) - } - if !cmp.Equal(groupLayer1.BoundingBoxes, tt.grouplayer1ExpectedBoundingBoxes) { - t.Errorf("Grouplayer has unexpected bounding boxes = %v, want %v", groupLayer1.BoundingBoxes, tt.grouplayer1ExpectedBoundingBoxes) - } - if len(dataLayer1.BoundingBoxes) != tt.datalayer1ExpectedBoundingBoxCount { - t.Errorf("Datalayer1 has unexpected number of bounding boxes = %v, want %v", len(dataLayer1.BoundingBoxes), tt.datalayer1ExpectedBoundingBoxCount) - } - if !cmp.Equal(dataLayer1.BoundingBoxes, tt.datalayer1ExpectedBoundingBoxes) { - t.Errorf("Datalayer1 has unexpected bounding boxes = %v, want %v", dataLayer1.BoundingBoxes, tt.datalayer1ExpectedBoundingBoxes) - } - if len(dataLayer2.BoundingBoxes) != tt.datalayer2ExpectedBoundingBoxCount { - t.Errorf("Datalayer2 has unexpected number of bounding boxes = %v, want %v", len(dataLayer2.BoundingBoxes), tt.datalayer2ExpectedBoundingBoxCount) - } - if !cmp.Equal(dataLayer2.BoundingBoxes, tt.datalayer2ExpectedBoundingBoxes) { - t.Errorf("Datalayer2 has unexpected bounding boxes = %v, want %v", dataLayer2.BoundingBoxes, tt.datalayer2ExpectedBoundingBoxes) - } - }) - } -} - -func TestLayer_GetParent(t *testing.T) { - childLayer2 := Layer{Name: smoothoperatorutils.Pointer("childlayer-2")} - childLayer1 := Layer{Name: smoothoperatorutils.Pointer("childlayer-1"), Layers: []Layer{childLayer2}} - topLayer := Layer{Name: smoothoperatorutils.Pointer("toplayer"), Layers: []Layer{childLayer1}} - - type args struct { - service WMSService - } - tests := []struct { - name string - layer Layer - args args - want *Layer - }{ - { - name: "Test GetParent on layer with parent", - layer: childLayer2, - args: args{service: WMSService{Layer: topLayer}}, - want: &childLayer1, - }, - { - name: "Test GetParent on layer without parent", - layer: topLayer, - args: args{service: WMSService{Layer: topLayer}}, - want: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := tt.args.service.GetParentLayer(tt.layer); !reflect.DeepEqual(got, tt.want) { - t.Errorf("GetParent() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/api/v3/wms_validation.go b/api/v3/wms_validation.go deleted file mode 100644 index ae02f98..0000000 --- a/api/v3/wms_validation.go +++ /dev/null @@ -1,334 +0,0 @@ -package v3 - -import ( - "fmt" - "strings" - - "sigs.k8s.io/controller-runtime/pkg/client" - - sharedValidation "github.com/pdok/smooth-operator/pkg/validation" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/utils/strings/slices" -) - -func (wms *WMS) ValidateCreate(c client.Client) ([]string, error) { - return ValidateCreate(c, wms, ValidateWMS) -} - -func (wms *WMS) ValidateUpdate(c client.Client, wmsOld *WMS) ([]string, error) { - return ValidateUpdate(c, wms, wmsOld, ValidateWMS) -} - -func ValidateWMS(wms *WMS, warnings *[]string, allErrs *field.ErrorList) { - if strings.Contains(wms.GetName(), "wms") { - sharedValidation.AddWarning( - warnings, - *field.NewPath("metadata").Child("name"), - "name should not contain wms", - wms.GroupVersionKind(), - wms.GetName(), - ) - } - - if wms.Mapfile() != nil { - service := wms.Spec.Service - path := field.NewPath("spec").Child("service") - if service.Resolution != nil { - sharedValidation.AddWarning( - warnings, - *path.Child("resolution"), - "not used when service.mapfile is configured", - wms.GroupVersionKind(), - wms.GetName(), - ) - } - if service.DefResolution != nil { - sharedValidation.AddWarning( - warnings, - *path.Child("defResolution"), - "not used when service.mapfile is configured", - wms.GroupVersionKind(), - wms.GetName(), - ) - } - } - - ValidateInspire(wms, allErrs, warnings) - if wms.HorizontalPodAutoscalerPatch() != nil { - ValidateHorizontalPodAutoscalerPatch(*wms.HorizontalPodAutoscalerPatch(), allErrs) - } - ValidateEphemeralStorage(wms.PodSpecPatch(), allErrs) - - validateLayers(wms, warnings, allErrs) -} - -func validateLayers(wms *WMS, warnings *[]string, allErrs *field.ErrorList) { - - layerNames := []string{} - hasVisibleLayer := false - - topLayer := AnnotatedLayer{ - GroupName: nil, - IsTopLayer: true, - IsGroupLayer: true, - IsDataLayer: false, - Layer: wms.Spec.Service.Layer, - } - - validateLayer(topLayer, field.NewPath("spec").Child("service").Child("layer"), []string{}, &layerNames, &hasVisibleLayer, wms, warnings, allErrs) - - if !hasVisibleLayer { - *allErrs = append(*allErrs, field.Required( - field.NewPath("spec").Child("service").Child("layer").Child("layers[*]").Child("visible"), - "at least one layer must be visible", - )) - } -} - -func validateLayer(layer AnnotatedLayer, path *field.Path, groupStyles []string, layerNames *[]string, hasVisibleLayer *bool, wms *WMS, warnings *[]string, allErrs *field.ErrorList) { - service := wms.Spec.Service - - var layerName string - if layer.IsTopLayer && layer.Name == nil { - layerName = "unnamed: " + TopLayer - } else { - layerName = *layer.Name - } - - if slices.Contains(*layerNames, layerName) { - *allErrs = append(*allErrs, field.Duplicate( - path.Child("name"), - layerName, - )) - } else { - *layerNames = append(*layerNames, layerName) - } - - if layer.IsGroupLayer && layer.Data != nil { - *allErrs = append(*allErrs, field.Invalid( - path.Child("data"), - layer.Data, - "must not be set on a GroupLayer", - )) - } - - validateLayerWithMapfile(layer, path, wms, warnings, allErrs) - - if layer.Visible { - if !layer.IsTopLayer { - *hasVisibleLayer = true - } - } else { - validateNotVisibleLayer(layer, path, wms, warnings, allErrs) - } - - crsses := []string{} - for i, bbox := range layer.BoundingBoxes { - if slices.Contains(crsses, bbox.CRS) { - *allErrs = append(*allErrs, field.Duplicate( - path.Child("boundingBoxes").Index(i).Child("crs"), - bbox.CRS, - )) - } else { - crsses = append(crsses, bbox.CRS) - } - } - - styleNames := []string{} - for i, style := range layer.Styles { - stylePath := path.Child("styles").Index(i) - validateStyle(style, stylePath, &styleNames, &groupStyles, service.StylingAssets.GetAllConfigMapRefKeys(), layer, service.Mapfile != nil, allErrs) - } - - if layer.IsDataLayer { - for _, groupStyle := range groupStyles { - if !slices.Contains(styleNames, groupStyle) { - *allErrs = append(*allErrs, field.Invalid( - path.Child("styles"), - nil, - fmt.Sprintf("dataLayer must implement style: %s, defined by a parent layer", groupStyle), - )) - } - } - } - - for i, subLayer := range layer.Layers { - annotatedSubLayer := AnnotatedLayer{ - GroupName: layer.Name, - IsTopLayer: false, - IsGroupLayer: subLayer.IsGroupLayer(), - IsDataLayer: subLayer.IsDataLayer(), - Layer: subLayer, - } - validateLayer(annotatedSubLayer, path.Child("layers").Index(i), groupStyles, layerNames, hasVisibleLayer, wms, warnings, allErrs) - } - -} - -func validateLayerWithMapfile(layer AnnotatedLayer, path *field.Path, wms *WMS, warnings *[]string, allErrs *field.ErrorList) { - service := wms.Spec.Service - hasCustomMapfile := service.Mapfile != nil - if hasCustomMapfile && layer.BoundingBoxes != nil { - sharedValidation.AddWarning( - warnings, - *path.Child("boundingBoxes"), - "is not used when service.mapfile is configured", - wms.GroupVersionKind(), - wms.GetName(), - ) - } - if !hasCustomMapfile && service.DataEPSG != "EPSG:28992" && !layer.hasBoundingBoxForCRS(service.DataEPSG) && layer.Name != nil { - *allErrs = append(*allErrs, field.Required( - path.Child("boundingBoxes").Child("crs"), - fmt.Sprintf("must contain a boundingBox for CRS %s when service.dataEPSG is not 'EPSG:28992'", service.DataEPSG), - )) - } - - if layer.IsDataLayer && hasCustomMapfile { - if tif := layer.Data.TIF; tif != nil { - tifWarnings(tif, path, wms, warnings) - } - } - -} - -func tifWarnings(tif *TIF, path *field.Path, wms *WMS, warnings *[]string) { - if tif.Resample != "NEAREST" { - sharedValidation.AddWarning( - warnings, - *path.Child("data").Child("tif").Child("resample"), - "is not used when service.mapfile is configured", - wms.GroupVersionKind(), - wms.GetName(), - ) - } - - if tif.Offsite != nil { - sharedValidation.AddWarning( - warnings, - *path.Child("data").Child("tif").Child("offsite"), - "is not used when service.mapfile is configured", - wms.GroupVersionKind(), - wms.GetName(), - ) - } - - if tif.GetFeatureInfoIncludesClass { - sharedValidation.AddWarning( - warnings, - *path.Child("data").Child("tif").Child("getFeatureInfoIncludesClass"), - "is not used when service.mapfile is configured", - wms.GroupVersionKind(), - wms.GetName(), - ) - } -} - -func validateStyle(style Style, path *field.Path, styleNames *[]string, groupStyles *[]string, stylingFiles []string, layer AnnotatedLayer, usesCustomMapfile bool, allErrs *field.ErrorList) { - if slices.Contains(*styleNames, style.Name) { - *allErrs = append(*allErrs, field.Invalid( - path.Child("name"), - style.Name, - "A Layer can't use the same style name multiple times", - )) - } else { - *styleNames = append(*styleNames, style.Name) - } - - if layer.Visible && !slices.Contains(*groupStyles, style.Name) && style.Title == nil { - *allErrs = append(*allErrs, field.Required( - path.Child("title"), - "A Style must have a title on the highest visible Layer", - )) - } - - if layer.IsGroupLayer { - if slices.Contains(*groupStyles, style.Name) { - *allErrs = append(*allErrs, field.Invalid( - path.Child("name"), - style.Name, - "A GroupLayer can't redefine the same style as a parent layer", - )) - } else { - *groupStyles = append(*groupStyles, style.Name) - } - - if style.Visualization != nil { - *allErrs = append(*allErrs, field.Invalid( - path.Child("visualization"), - style.Visualization, - "GroupLayers must not have a visualization", - )) - } - } - - if layer.IsDataLayer { - switch { - case usesCustomMapfile && style.Visualization != nil: - *allErrs = append(*allErrs, field.Invalid( - path.Child("visualization"), - style.Visualization, - "is not used when spec.service.mapfile is used", - )) - case !usesCustomMapfile && style.Visualization == nil: - *allErrs = append(*allErrs, field.Required( - path.Child("visualization"), - "on DataLayers when spec.service.mapfile is not used", - )) - case !usesCustomMapfile && !slices.Contains(stylingFiles, *style.Visualization): - *allErrs = append(*allErrs, field.Invalid( - path.Child("visualization"), - style.Visualization, - "must be defined be in spec.service.stylingAssets.configMapKeyRefs.Keys", - )) - } - - } -} - -func validateNotVisibleLayer(layer AnnotatedLayer, path *field.Path, wms *WMS, warnings *[]string, allErrs *field.ErrorList) { - if layer.IsGroupLayer { - *allErrs = append(*allErrs, field.Invalid( - path.Child("visible"), - layer.Visible, - "must be true for a "+GroupLayer, - )) - } - paths := []field.Path{} - - if layer.Title != nil { - paths = append(paths, *path.Child("title")) - } - if layer.Abstract != nil { - paths = append(paths, *path.Child("abstract")) - } - if layer.Keywords != nil { - paths = append(paths, *path.Child("keywords")) - } - if layer.DatasetMetadataURL != nil { - paths = append(paths, *path.Child("datasetMetadataURL")) - } - if layer.Authority != nil { - paths = append(paths, *path.Child("authority")) - } - - for i, style := range layer.Styles { - if style.Title != nil { - paths = append(paths, *path.Child("styles").Index(i).Child("title")) - } - if style.Abstract != nil { - paths = append(paths, *path.Child("styles").Index(i).Child("abstract")) - } - } - - for _, path := range paths { - sharedValidation.AddWarning( - warnings, - path, - "is not used when layer.visible=false", - wms.GroupVersionKind(), - wms.GetName(), - ) - } - -} diff --git a/api/v3/zz_generated.deepcopy.go b/api/v3/zz_generated.deepcopy.go index 137e8a6..e2bdc59 100644 --- a/api/v3/zz_generated.deepcopy.go +++ b/api/v3/zz_generated.deepcopy.go @@ -1,27 +1,19 @@ //go:build !ignore_autogenerated /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ // Code generated by controller-gen. DO NOT EDIT. @@ -29,666 +21,16 @@ SOFTWARE. package v3 import ( - "github.com/pdok/smooth-operator/model" - "k8s.io/api/autoscaling/v2" runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AnnotatedLayer) DeepCopyInto(out *AnnotatedLayer) { - *out = *in - if in.GroupName != nil { - in, out := &in.GroupName, &out.GroupName - *out = new(string) - **out = **in - } - in.Layer.DeepCopyInto(&out.Layer) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnnotatedLayer. -func (in *AnnotatedLayer) DeepCopy() *AnnotatedLayer { - if in == nil { - return nil - } - out := new(AnnotatedLayer) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Authority) DeepCopyInto(out *Authority) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authority. -func (in *Authority) DeepCopy() *Authority { - if in == nil { - return nil - } - out := new(Authority) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BaseData) DeepCopyInto(out *BaseData) { - *out = *in - if in.Gpkg != nil { - in, out := &in.Gpkg, &out.Gpkg - *out = new(Gpkg) - (*in).DeepCopyInto(*out) - } - if in.Postgis != nil { - in, out := &in.Postgis, &out.Postgis - *out = new(Postgis) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseData. -func (in *BaseData) DeepCopy() *BaseData { - if in == nil { - return nil - } - out := new(BaseData) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BaseOptions) DeepCopyInto(out *BaseOptions) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseOptions. -func (in *BaseOptions) DeepCopy() *BaseOptions { - if in == nil { - return nil - } - out := new(BaseOptions) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BaseService) DeepCopyInto(out *BaseService) { - *out = *in - in.URL.DeepCopyInto(&out.URL) - if in.Mapfile != nil { - in, out := &in.Mapfile, &out.Mapfile - *out = new(Mapfile) - (*in).DeepCopyInto(*out) - } - if in.Keywords != nil { - in, out := &in.Keywords, &out.Keywords - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Fees != nil { - in, out := &in.Fees, &out.Fees - *out = new(string) - **out = **in - } - in.AccessConstraints.DeepCopyInto(&out.AccessConstraints) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseService. -func (in *BaseService) DeepCopy() *BaseService { - if in == nil { - return nil - } - out := new(BaseService) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Bbox) DeepCopyInto(out *Bbox) { - *out = *in - out.DefaultCRS = in.DefaultCRS -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bbox. -func (in *Bbox) DeepCopy() *Bbox { - if in == nil { - return nil - } - out := new(Bbox) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Column) DeepCopyInto(out *Column) { - *out = *in - if in.Alias != nil { - in, out := &in.Alias, &out.Alias - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Column. -func (in *Column) DeepCopy() *Column { - if in == nil { - return nil - } - out := new(Column) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ConfigMapRef) DeepCopyInto(out *ConfigMapRef) { - *out = *in - if in.Keys != nil { - in, out := &in.Keys, &out.Keys - *out = make([]string, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapRef. -func (in *ConfigMapRef) DeepCopy() *ConfigMapRef { - if in == nil { - return nil - } - out := new(ConfigMapRef) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Custom) DeepCopyInto(out *Custom) { - *out = *in - in.Href.DeepCopyInto(&out.Href) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Custom. -func (in *Custom) DeepCopy() *Custom { - if in == nil { - return nil - } - out := new(Custom) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Data) DeepCopyInto(out *Data) { - *out = *in - in.BaseData.DeepCopyInto(&out.BaseData) - if in.TIF != nil { - in, out := &in.TIF, &out.TIF - *out = new(TIF) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Data. -func (in *Data) DeepCopy() *Data { - if in == nil { - return nil - } - out := new(Data) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FeatureBbox) DeepCopyInto(out *FeatureBbox) { - *out = *in - if in.DefaultCRS != nil { - in, out := &in.DefaultCRS, &out.DefaultCRS - *out = new(model.BBox) - **out = **in - } - if in.WGS84 != nil { - in, out := &in.WGS84, &out.WGS84 - *out = new(model.BBox) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureBbox. -func (in *FeatureBbox) DeepCopy() *FeatureBbox { - if in == nil { - return nil - } - out := new(FeatureBbox) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FeatureType) DeepCopyInto(out *FeatureType) { - *out = *in - if in.Keywords != nil { - in, out := &in.Keywords, &out.Keywords - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.DatasetMetadataURL != nil { - in, out := &in.DatasetMetadataURL, &out.DatasetMetadataURL - *out = new(MetadataURL) - (*in).DeepCopyInto(*out) - } - if in.Bbox != nil { - in, out := &in.Bbox, &out.Bbox - *out = new(FeatureBbox) - (*in).DeepCopyInto(*out) - } - in.Data.DeepCopyInto(&out.Data) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureType. -func (in *FeatureType) DeepCopy() *FeatureType { - if in == nil { - return nil - } - out := new(FeatureType) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Gpkg) DeepCopyInto(out *Gpkg) { - *out = *in - if in.Columns != nil { - in, out := &in.Columns, &out.Columns - *out = make([]Column, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Gpkg. -func (in *Gpkg) DeepCopy() *Gpkg { - if in == nil { - return nil - } - out := new(Gpkg) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HealthCheckWFS) DeepCopyInto(out *HealthCheckWFS) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckWFS. -func (in *HealthCheckWFS) DeepCopy() *HealthCheckWFS { - if in == nil { - return nil - } - out := new(HealthCheckWFS) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HealthCheckWMS) DeepCopyInto(out *HealthCheckWMS) { - *out = *in - if in.Querystring != nil { - in, out := &in.Querystring, &out.Querystring - *out = new(string) - **out = **in - } - if in.Mimetype != nil { - in, out := &in.Mimetype, &out.Mimetype - *out = new(string) - **out = **in - } - if in.Boundingbox != nil { - in, out := &in.Boundingbox, &out.Boundingbox - *out = new(model.BBox) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckWMS. -func (in *HealthCheckWMS) DeepCopy() *HealthCheckWMS { - if in == nil { - return nil - } - out := new(HealthCheckWMS) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HorizontalPodAutoscalerPatch) DeepCopyInto(out *HorizontalPodAutoscalerPatch) { - *out = *in - if in.MinReplicas != nil { - in, out := &in.MinReplicas, &out.MinReplicas - *out = new(int32) - **out = **in - } - if in.MaxReplicas != nil { - in, out := &in.MaxReplicas, &out.MaxReplicas - *out = new(int32) - **out = **in - } - if in.Metrics != nil { - in, out := &in.Metrics, &out.Metrics - *out = make([]v2.MetricSpec, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Behavior != nil { - in, out := &in.Behavior, &out.Behavior - *out = new(v2.HorizontalPodAutoscalerBehavior) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalPodAutoscalerPatch. -func (in *HorizontalPodAutoscalerPatch) DeepCopy() *HorizontalPodAutoscalerPatch { - if in == nil { - return nil - } - out := new(HorizontalPodAutoscalerPatch) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Inspire) DeepCopyInto(out *Inspire) { - *out = *in - in.ServiceMetadataURL.DeepCopyInto(&out.ServiceMetadataURL) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Inspire. -func (in *Inspire) DeepCopy() *Inspire { - if in == nil { - return nil - } - out := new(Inspire) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Layer) DeepCopyInto(out *Layer) { - *out = *in - if in.Name != nil { - in, out := &in.Name, &out.Name - *out = new(string) - **out = **in - } - if in.Title != nil { - in, out := &in.Title, &out.Title - *out = new(string) - **out = **in - } - if in.Abstract != nil { - in, out := &in.Abstract, &out.Abstract - *out = new(string) - **out = **in - } - if in.Keywords != nil { - in, out := &in.Keywords, &out.Keywords - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.BoundingBoxes != nil { - in, out := &in.BoundingBoxes, &out.BoundingBoxes - *out = make([]WMSBoundingBox, len(*in)) - copy(*out, *in) - } - if in.Authority != nil { - in, out := &in.Authority, &out.Authority - *out = new(Authority) - **out = **in - } - if in.DatasetMetadataURL != nil { - in, out := &in.DatasetMetadataURL, &out.DatasetMetadataURL - *out = new(MetadataURL) - (*in).DeepCopyInto(*out) - } - if in.MinScaleDenominator != nil { - in, out := &in.MinScaleDenominator, &out.MinScaleDenominator - *out = new(string) - **out = **in - } - if in.MaxScaleDenominator != nil { - in, out := &in.MaxScaleDenominator, &out.MaxScaleDenominator - *out = new(string) - **out = **in - } - if in.Styles != nil { - in, out := &in.Styles, &out.Styles - *out = make([]Style, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.Data != nil { - in, out := &in.Data, &out.Data - *out = new(Data) - (*in).DeepCopyInto(*out) - } - if in.Layers != nil { - in, out := &in.Layers, &out.Layers - *out = make([]Layer, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Layer. -func (in *Layer) DeepCopy() *Layer { - if in == nil { - return nil - } - out := new(Layer) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Legend) DeepCopyInto(out *Legend) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Legend. -func (in *Legend) DeepCopy() *Legend { - if in == nil { - return nil - } - out := new(Legend) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Mapfile) DeepCopyInto(out *Mapfile) { - *out = *in - in.ConfigMapKeyRef.DeepCopyInto(&out.ConfigMapKeyRef) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mapfile. -func (in *Mapfile) DeepCopy() *Mapfile { - if in == nil { - return nil - } - out := new(Mapfile) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Metadata) DeepCopyInto(out *Metadata) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metadata. -func (in *Metadata) DeepCopy() *Metadata { - if in == nil { - return nil - } - out := new(Metadata) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MetadataURL) DeepCopyInto(out *MetadataURL) { - *out = *in - if in.CSW != nil { - in, out := &in.CSW, &out.CSW - *out = new(Metadata) - **out = **in - } - if in.Custom != nil { - in, out := &in.Custom, &out.Custom - *out = new(Custom) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetadataURL. -func (in *MetadataURL) DeepCopy() *MetadataURL { - if in == nil { - return nil - } - out := new(MetadataURL) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Options) DeepCopyInto(out *Options) { - *out = *in - out.BaseOptions = in.BaseOptions - out.WMSOptions = in.WMSOptions -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Options. -func (in *Options) DeepCopy() *Options { - if in == nil { - return nil - } - out := new(Options) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Postgis) DeepCopyInto(out *Postgis) { - *out = *in - if in.Columns != nil { - in, out := &in.Columns, &out.Columns - *out = make([]Column, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgis. -func (in *Postgis) DeepCopy() *Postgis { - if in == nil { - return nil - } - out := new(Postgis) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Style) DeepCopyInto(out *Style) { - *out = *in - if in.Title != nil { - in, out := &in.Title, &out.Title - *out = new(string) - **out = **in - } - if in.Abstract != nil { - in, out := &in.Abstract, &out.Abstract - *out = new(string) - **out = **in - } - if in.Visualization != nil { - in, out := &in.Visualization, &out.Visualization - *out = new(string) - **out = **in - } - if in.Legend != nil { - in, out := &in.Legend, &out.Legend - *out = new(Legend) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Style. -func (in *Style) DeepCopy() *Style { - if in == nil { - return nil - } - out := new(Style) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *StylingAssets) DeepCopyInto(out *StylingAssets) { - *out = *in - if in.BlobKeys != nil { - in, out := &in.BlobKeys, &out.BlobKeys - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.ConfigMapRefs != nil { - in, out := &in.ConfigMapRefs, &out.ConfigMapRefs - *out = make([]ConfigMapRef, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StylingAssets. -func (in *StylingAssets) DeepCopy() *StylingAssets { - if in == nil { - return nil - } - out := new(StylingAssets) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TIF) DeepCopyInto(out *TIF) { - *out = *in - if in.Offsite != nil { - in, out := &in.Offsite, &out.Offsite - *out = new(string) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TIF. -func (in *TIF) DeepCopy() *TIF { - if in == nil { - return nil - } - out := new(TIF) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WFS) DeepCopyInto(out *WFS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) + out.Spec = in.Spec + out.Status = in.Status } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFS. @@ -709,22 +51,6 @@ func (in *WFS) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSInspire) DeepCopyInto(out *WFSInspire) { - *out = *in - in.Inspire.DeepCopyInto(&out.Inspire) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSInspire. -func (in *WFSInspire) DeepCopy() *WFSInspire { - if in == nil { - return nil - } - out := new(WFSInspire) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WFSList) DeepCopyInto(out *WFSList) { *out = *in @@ -758,88 +84,31 @@ func (in *WFSList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSService) DeepCopyInto(out *WFSService) { +func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { *out = *in - in.BaseService.DeepCopyInto(&out.BaseService) - if in.Inspire != nil { - in, out := &in.Inspire, &out.Inspire - *out = new(WFSInspire) - (*in).DeepCopyInto(*out) - } - if in.OtherCrs != nil { - in, out := &in.OtherCrs, &out.OtherCrs - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.Bbox != nil { - in, out := &in.Bbox, &out.Bbox - *out = new(Bbox) - **out = **in - } - if in.CountDefault != nil { - in, out := &in.CountDefault, &out.CountDefault - *out = new(int) - **out = **in - } - if in.FeatureTypes != nil { - in, out := &in.FeatureTypes, &out.FeatureTypes - *out = make([]FeatureType, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSService. -func (in *WFSService) DeepCopy() *WFSService { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. +func (in *WFSSpec) DeepCopy() *WFSSpec { if in == nil { return nil } - out := new(WFSService) + out := new(WFSSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { +func (in *WFSStatus) DeepCopyInto(out *WFSStatus) { *out = *in - if in.Lifecycle != nil { - in, out := &in.Lifecycle, &out.Lifecycle - *out = new(model.Lifecycle) - (*in).DeepCopyInto(*out) - } - in.PodSpecPatch.DeepCopyInto(&out.PodSpecPatch) - if in.HorizontalPodAutoscalerPatch != nil { - in, out := &in.HorizontalPodAutoscalerPatch, &out.HorizontalPodAutoscalerPatch - *out = new(HorizontalPodAutoscalerPatch) - (*in).DeepCopyInto(*out) - } - if in.Options != nil { - in, out := &in.Options, &out.Options - *out = new(BaseOptions) - **out = **in - } - if in.HealthCheck != nil { - in, out := &in.HealthCheck, &out.HealthCheck - *out = new(HealthCheckWFS) - **out = **in - } - if in.IngressRouteURLs != nil { - in, out := &in.IngressRouteURLs, &out.IngressRouteURLs - *out = make(model.IngressRouteURLs, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - in.Service.DeepCopyInto(&out.Service) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. -func (in *WFSSpec) DeepCopy() *WFSSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSStatus. +func (in *WFSStatus) DeepCopy() *WFSStatus { if in == nil { return nil } - out := new(WFSSpec) + out := new(WFSStatus) in.DeepCopyInto(out) return out } @@ -849,8 +118,8 @@ func (in *WMS) DeepCopyInto(out *WMS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) + out.Spec = in.Spec + out.Status = in.Status } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMS. @@ -871,22 +140,6 @@ func (in *WMS) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSBoundingBox) DeepCopyInto(out *WMSBoundingBox) { - *out = *in - out.BBox = in.BBox -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSBoundingBox. -func (in *WMSBoundingBox) DeepCopy() *WMSBoundingBox { - if in == nil { - return nil - } - out := new(WMSBoundingBox) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WMSList) DeepCopyInto(out *WMSList) { *out = *in @@ -920,107 +173,31 @@ func (in *WMSList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSOptions) DeepCopyInto(out *WMSOptions) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSOptions. -func (in *WMSOptions) DeepCopy() *WMSOptions { - if in == nil { - return nil - } - out := new(WMSOptions) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSService) DeepCopyInto(out *WMSService) { +func (in *WMSSpec) DeepCopyInto(out *WMSSpec) { *out = *in - in.BaseService.DeepCopyInto(&out.BaseService) - if in.Inspire != nil { - in, out := &in.Inspire, &out.Inspire - *out = new(Inspire) - (*in).DeepCopyInto(*out) - } - if in.MaxSize != nil { - in, out := &in.MaxSize, &out.MaxSize - *out = new(int32) - **out = **in - } - if in.Resolution != nil { - in, out := &in.Resolution, &out.Resolution - *out = new(int32) - **out = **in - } - if in.DefResolution != nil { - in, out := &in.DefResolution, &out.DefResolution - *out = new(int32) - **out = **in - } - if in.StylingAssets != nil { - in, out := &in.StylingAssets, &out.StylingAssets - *out = new(StylingAssets) - (*in).DeepCopyInto(*out) - } - if in.Mapfile != nil { - in, out := &in.Mapfile, &out.Mapfile - *out = new(Mapfile) - (*in).DeepCopyInto(*out) - } - in.Layer.DeepCopyInto(&out.Layer) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSService. -func (in *WMSService) DeepCopy() *WMSService { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSSpec. +func (in *WMSSpec) DeepCopy() *WMSSpec { if in == nil { return nil } - out := new(WMSService) + out := new(WMSSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSSpec) DeepCopyInto(out *WMSSpec) { +func (in *WMSStatus) DeepCopyInto(out *WMSStatus) { *out = *in - if in.Lifecycle != nil { - in, out := &in.Lifecycle, &out.Lifecycle - *out = new(model.Lifecycle) - (*in).DeepCopyInto(*out) - } - in.PodSpecPatch.DeepCopyInto(&out.PodSpecPatch) - if in.HorizontalPodAutoscalerPatch != nil { - in, out := &in.HorizontalPodAutoscalerPatch, &out.HorizontalPodAutoscalerPatch - *out = new(HorizontalPodAutoscalerPatch) - (*in).DeepCopyInto(*out) - } - if in.Options != nil { - in, out := &in.Options, &out.Options - *out = new(Options) - **out = **in - } - if in.HealthCheck != nil { - in, out := &in.HealthCheck, &out.HealthCheck - *out = new(HealthCheckWMS) - (*in).DeepCopyInto(*out) - } - if in.IngressRouteURLs != nil { - in, out := &in.IngressRouteURLs, &out.IngressRouteURLs - *out = make(model.IngressRouteURLs, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - in.Service.DeepCopyInto(&out.Service) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSSpec. -func (in *WMSSpec) DeepCopy() *WMSSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSStatus. +func (in *WMSStatus) DeepCopy() *WMSStatus { if in == nil { return nil } - out := new(WMSSpec) + out := new(WMSStatus) in.DeepCopyInto(out) return out } diff --git a/build-push-deploy-locally.sh b/build-push-deploy-locally.sh deleted file mode 100755 index cc44f30..0000000 --- a/build-push-deploy-locally.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -TAG=$1 - -echo "Running: make generate" -make generate - -echo "" -echo "Running: build -t local-registry:5000/mapserver-operator:$TAG --build-context repos=./.. ." -docker build -t "local-registry:5000/mapserver-operator:$TAG" --build-context repos=./.. . - -echo "" -echo "Running: push local-registry:5000/mapserver-operator:$TAG" -docker push "local-registry:5000/mapserver-operator:$TAG" - -if [[ $(kubectl get pod -l app=webhook -n cert-manager | grep "cert-manager") ]]; then - echo "Cert-manager already installed" -else - echo "" - echo "Installing cert-manager" - kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml -fi - -echo "Waiting for cert-manager" -while [[ $(kubectl get pod -l app=webhook -n cert-manager -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" ]]; do - sleep 1 -done -echo "Cert-manager ready" - -echo "" -echo "Running: make install" -make install - -echo "" -echo "Running: deploy IMG=local-registry:5000/mapserver-operator:$TAG" -make deploy "IMG=local-registry:5000/mapserver-operator:$TAG" \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 7d195f4..df0355c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,21 +18,9 @@ package main import ( "crypto/tls" - "errors" "flag" "os" - - "github.com/pdok/mapserver-operator/internal/controller/types" - - "github.com/go-logr/zapr" - "github.com/pdok/smooth-operator/pkg/integrations/logging" - "github.com/peterbourgon/ff" - "go.uber.org/zap/zapcore" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/pdok/mapserver-operator/internal/controller/mapfilegenerator" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + "path/filepath" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -42,20 +30,20 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/certwatcher" "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + pdoknlv2beta1 "github.com/pdok/mapserver-operator/api/v2beta1" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" "github.com/pdok/mapserver-operator/internal/controller" webhookpdoknlv3 "github.com/pdok/mapserver-operator/internal/webhook/v3" // +kubebuilder:scaffold:imports ) -const ( - EnvFalse = "false" -) - var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -63,29 +51,23 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(traefikiov1alpha1.AddToScheme(scheme)) - utilruntime.Must(smoothoperatorv1.AddToScheme(scheme)) + utilruntime.Must(pdoknlv3.AddToScheme(scheme)) + utilruntime.Must(pdoknlv2beta1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } -//nolint:funlen +// nolint:gocyclo func main() { var metricsAddr string - var certDir string + var metricsCertPath, metricsCertName, metricsCertKey string + var webhookCertPath, webhookCertName, webhookCertKey string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) - var host string - var mapserverDebugLevel int - var multitoolImage, mapfileGeneratorImage, mapserverImage, capabilitiesGeneratorImage, featureinfoGeneratorImage, ogcWebserviceProxyImage, apacheExporterImage string - var slackWebhookURL string - var logLevel int - var setUptimeOperatorAnnotations bool - var storageClassName string - flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metrics endpoint binds to. "+ + flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -93,60 +75,22 @@ func main() { "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.StringVar(&certDir, "cert-dir", "", "CertDir contains the webhook server key and certificate. Defaults to /k8s-webhook-server/serving-certs.") + flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", + "The directory that contains the metrics server certificate.") + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") - flag.StringVar(&host, "baseurl", "", "The host which is used in the mapserver service.") - flag.StringVar(&multitoolImage, "multitool-image", "", "The image to use in the blob download init-container.") - flag.StringVar(&mapfileGeneratorImage, "mapfile-generator-image", "", "The image to use in the mapfile generator init-container.") - flag.StringVar(&mapserverImage, "mapserver-image", "", "The image to use in the mapserver container.") - flag.StringVar(&capabilitiesGeneratorImage, "capabilities-generator-image", "", "The image to use in the capabilities generator init-container.") - flag.StringVar(&featureinfoGeneratorImage, "featureinfo-generator-image", "", "The image to use in the featureinfo generator init-container.") - flag.StringVar(&ogcWebserviceProxyImage, "ogc-webservice-proxy-image", "", "The image to use in the ogc webservice proxy container.") - flag.StringVar(&apacheExporterImage, "apache-exporter-image", "", "The image to use in the apache-exporter container.") - flag.IntVar(&mapserverDebugLevel, "mapserver-debug-level", 0, "Debug level for the mapserver container, between 0 (error only) and 5 (very very verbose).") - flag.StringVar(&slackWebhookURL, "slack-webhook-url", "", "The webhook url for sending slack messages. Disabled if left empty") - flag.IntVar(&logLevel, "log-level", 0, "The zapcore loglevel. 0 = info, 1 = warn, 2 = error") - flag.BoolVar(&setUptimeOperatorAnnotations, "set-uptime-operator-annotations", true, "When enabled IngressRoutes get annotations that are used by the pdok/uptime-operator.") - flag.StringVar(&storageClassName, "storage-class-name", "", "The name of the storage class to use when using an ephemeral volume.") - opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) + flag.Parse() - if err := ff.Parse(flag.CommandLine, os.Args[1:], ff.WithEnvVarNoPrefix()); err != nil { - setupLog.Error(err, "unable to parse flags") - os.Exit(1) - } - - //nolint:gosec - levelEnabler := zapcore.Level(logLevel) - zapLogger, _ := logging.SetupLogger("mapserver-operator", slackWebhookURL, levelEnabler) - logrLogger := zapr.NewLogger(zapLogger) - ctrl.SetLogger(logrLogger) - - reqFlags := make(map[string]string) - reqFlags["baseurl"] = host - reqFlags["multitool-image"] = multitoolImage - reqFlags["mapfile-generator-image"] = mapfileGeneratorImage - reqFlags["mapserver-image"] = mapserverImage - reqFlags["capabilities-generator-image"] = capabilitiesGeneratorImage - reqFlags["featureinfo-generator-image"] = featureinfoGeneratorImage - reqFlags["ogc-webservice-proxy-image"] = ogcWebserviceProxyImage - reqFlags["apache-exporter-image"] = apacheExporterImage - - for reqFlag, val := range reqFlags { - if val == "" { - setupLog.Error(errors.New(reqFlag+" is a required flag"), "A value for "+reqFlag+" must be specified.") - os.Exit(1) - } - } - - pdoknlv3.SetHost(host) - mapfilegenerator.SetDebugLevel(mapserverDebugLevel) - controller.SetUptimeOperatorAnnotations(setUptimeOperatorAnnotations) - controller.SetStorageClassName(storageClassName) + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will @@ -163,18 +107,83 @@ func main() { tlsOpts = append(tlsOpts, disableHTTP2) } + // Create watchers for metrics and webhooks certificates + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + + if len(webhookCertPath) > 0 { + setupLog.Info("Initializing webhook certificate watcher using provided certificates", + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) + + var err error + webhookCertWatcher, err = certwatcher.New( + filepath.Join(webhookCertPath, webhookCertName), + filepath.Join(webhookCertPath, webhookCertKey), + ) + if err != nil { + setupLog.Error(err, "Failed to initialize webhook certificate watcher") + os.Exit(1) + } + + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = webhookCertWatcher.GetCertificate + }) + } + webhookServer := webhook.NewServer(webhook.Options{ - CertDir: certDir, - TLSOpts: tlsOpts, + TLSOpts: webhookTLSOpts, }) + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. + // More info: + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/server + // - https://book.kubebuilder.io/reference/metrics.html + metricsServerOptions := metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + } + + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization + } + + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(metricsCertPath) > 0 { + setupLog.Info("Initializing metrics certificate watcher using provided certificates", + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) + + var err error + metricsCertWatcher, err = certwatcher.New( + filepath.Join(metricsCertPath, metricsCertName), + filepath.Join(metricsCertPath, metricsCertKey), + ) + if err != nil { + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) + os.Exit(1) + } + + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { + config.GetCertificate = metricsCertWatcher.GetCertificate + }) + } + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, - Metrics: metricsserver.Options{ - BindAddress: metricsAddr, - SecureServing: secureMetrics, - TLSOpts: tlsOpts, - }, + Scheme: scheme, + Metrics: metricsServerOptions, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, @@ -199,15 +208,6 @@ func main() { if err = (&controller.WMSReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Images: types.Images{ - MultitoolImage: multitoolImage, - MapfileGeneratorImage: mapfileGeneratorImage, - MapserverImage: mapserverImage, - CapabilitiesGeneratorImage: capabilitiesGeneratorImage, - FeatureinfoGeneratorImage: featureinfoGeneratorImage, - OgcWebserviceProxyImage: ogcWebserviceProxyImage, - ApacheExporterImage: apacheExporterImage, - }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "WMS") os.Exit(1) @@ -215,33 +215,41 @@ func main() { if err = (&controller.WFSReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Images: types.Images{ - MultitoolImage: multitoolImage, - MapfileGeneratorImage: mapfileGeneratorImage, - MapserverImage: mapserverImage, - CapabilitiesGeneratorImage: capabilitiesGeneratorImage, - ApacheExporterImage: apacheExporterImage, - }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "WFS") os.Exit(1) } - - if os.Getenv("ENABLE_WEBHOOKS") != EnvFalse { + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err = webhookpdoknlv3.SetupWMSWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "WMS") + os.Exit(1) + } + } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err = webhookpdoknlv3.SetupWFSWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "WFS") os.Exit(1) } } + // +kubebuilder:scaffold:builder - if os.Getenv("ENABLE_WEBHOOKS") != EnvFalse { - if err = webhookpdoknlv3.SetupWMSWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "WMS") + if metricsCertWatcher != nil { + setupLog.Info("Adding metrics certificate watcher to manager") + if err := mgr.Add(metricsCertWatcher); err != nil { + setupLog.Error(err, "unable to add metrics certificate watcher to manager") os.Exit(1) } } - // +kubebuilder:scaffold:builder + if webhookCertWatcher != nil { + setupLog.Info("Adding webhook certificate watcher to manager") + if err := mgr.Add(webhookCertWatcher); err != nil { + setupLog.Error(err, "unable to add webhook certificate watcher to manager") + os.Exit(1) + } + } if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") diff --git a/config/crd/bases/embed.go b/config/crd/bases/embed.go deleted file mode 100644 index 45044dd..0000000 --- a/config/crd/bases/embed.go +++ /dev/null @@ -1,51 +0,0 @@ -package bases - -import ( - _ "embed" - - "github.com/pdok/smooth-operator/pkg/validation" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "sigs.k8s.io/yaml" -) - -//go:embed pdok.nl_wfs.yaml -var wfsCRD []byte - -//go:embed pdok.nl_wms.yaml -var wmsCRD []byte - -func init() { - wms, err := GetWmsCRD() - if err != nil { - panic(err) - } - - err = validation.AddValidator(wms) - if err != nil { - panic(err) - } - - wfs, err := GetWfsCRD() - if err != nil { - panic(err) - } - - err = validation.AddValidator(wfs) - if err != nil { - panic(err) - } -} - -func GetWmsCRD() (v1.CustomResourceDefinition, error) { - crd := v1.CustomResourceDefinition{} - err := yaml.Unmarshal(wmsCRD, &crd) - - return crd, err -} - -func GetWfsCRD() (v1.CustomResourceDefinition, error) { - crd := v1.CustomResourceDefinition{} - err := yaml.Unmarshal(wfsCRD, &crd) - - return crd, err -} diff --git a/config/crd/bases/pdok.nl_wfs.yaml b/config/crd/bases/pdok.nl_wfs.yaml deleted file mode 100644 index 2d9378f..0000000 --- a/config/crd/bases/pdok.nl_wfs.yaml +++ /dev/null @@ -1,1185 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.17.1 - creationTimestamp: null - name: wfs.pdok.nl -spec: - group: pdok.nl - names: - categories: - - pdok - kind: WFS - listKind: WFSList - plural: wfs - singular: wfs - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.podSummary[0].ready - name: ReadyPods - type: integer - - jsonPath: .status.podSummary[0].total - name: DesiredPods - type: integer - - jsonPath: .status.conditions[?(@.type == "Reconciled")].reason - name: ReconcileStatus - type: string - name: v3 - schema: - openAPIV3Schema: - description: WFS is the Schema for the wfs API. - 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: WFSSpec vertegenwoordigt de hoofdstruct voor de YAML-configuratie - properties: - healthCheck: - description: Custom healthcheck options - properties: - mimetype: - pattern: (image/png|text/xml|text/html) - type: string - querystring: - type: string - x-kubernetes-validations: - - message: a valid healthcheck contains 'Service=WFS' - rule: self.lowerAscii().contains('service=wfs') - - message: a valid healthcheck contains 'Request=' - rule: self.lowerAscii().contains('request=') - required: - - mimetype - - querystring - type: object - horizontalPodAutoscalerPatch: - description: |- - HorizontalPodAutoscalerPatch - copy of autoscalingv2.HorizontalPodAutoscalerSpec without ScaleTargetRef - This way we don't have to specify the scaleTargetRef field in the CRD. - properties: - behavior: - description: |- - HorizontalPodAutoscalerBehavior configures the scaling behavior of the target - in both Up and Down directions (scaleUp and scaleDown fields respectively). - properties: - scaleDown: - description: |- - scaleDown is scaling policy for scaling Down. - If not set, the default value is to allow to scale down to minReplicas pods, with a - 300 second stabilization window (i.e., the highest recommendation for - the last 300sec is used). - properties: - policies: - description: |- - policies is a list of potential scaling polices which can be used during scaling. - If not set, use the default values: - - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. - - For scale down: allow all pods to be removed in a 15s window. - items: - description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. - properties: - periodSeconds: - description: |- - periodSeconds specifies the window of time for which the policy should hold true. - PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). - format: int32 - type: integer - type: - description: type is used to specify the scaling policy. - type: string - value: - description: |- - value contains the amount of change which is permitted by the policy. - It must be greater than zero - format: int32 - type: integer - required: - - periodSeconds - - type - - value - type: object - type: array - x-kubernetes-list-type: atomic - selectPolicy: - description: |- - selectPolicy is used to specify which policy should be used. - If not set, the default value Max is used. - type: string - stabilizationWindowSeconds: - description: |- - stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up or scaling down. - StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). - If not set, use the default values: - - For scale up: 0 (i.e. no stabilization is done). - - For scale down: 300 (i.e. the stabilization window is 300 seconds long). - format: int32 - type: integer - tolerance: - anyOf: - - type: integer - - type: string - description: |- - tolerance is the tolerance on the ratio between the current and desired - metric value under which no updates are made to the desired number of - replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not - set, the default cluster-wide tolerance is applied (by default 10%). - - For example, if autoscaling is configured with a memory consumption target of 100Mi, - and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be - triggered when the actual consumption falls below 95Mi or exceeds 101Mi. - - This is an alpha field and requires enabling the HPAConfigurableTolerance - feature gate. - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - scaleUp: - description: |- - scaleUp is scaling policy for scaling Up. - If not set, the default value is the higher of: - * increase no more than 4 pods per 60 seconds - * double the number of pods per 60 seconds - No stabilization is used. - properties: - policies: - description: |- - policies is a list of potential scaling polices which can be used during scaling. - If not set, use the default values: - - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. - - For scale down: allow all pods to be removed in a 15s window. - items: - description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. - properties: - periodSeconds: - description: |- - periodSeconds specifies the window of time for which the policy should hold true. - PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). - format: int32 - type: integer - type: - description: type is used to specify the scaling policy. - type: string - value: - description: |- - value contains the amount of change which is permitted by the policy. - It must be greater than zero - format: int32 - type: integer - required: - - periodSeconds - - type - - value - type: object - type: array - x-kubernetes-list-type: atomic - selectPolicy: - description: |- - selectPolicy is used to specify which policy should be used. - If not set, the default value Max is used. - type: string - stabilizationWindowSeconds: - description: |- - stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up or scaling down. - StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). - If not set, use the default values: - - For scale up: 0 (i.e. no stabilization is done). - - For scale down: 300 (i.e. the stabilization window is 300 seconds long). - format: int32 - type: integer - tolerance: - anyOf: - - type: integer - - type: string - description: |- - tolerance is the tolerance on the ratio between the current and desired - metric value under which no updates are made to the desired number of - replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not - set, the default cluster-wide tolerance is applied (by default 10%). - - For example, if autoscaling is configured with a memory consumption target of 100Mi, - and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be - triggered when the actual consumption falls below 95Mi or exceeds 101Mi. - - This is an alpha field and requires enabling the HPAConfigurableTolerance - feature gate. - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - type: object - maxReplicas: - format: int32 - type: integer - metrics: - items: - description: |- - MetricSpec specifies how to scale based on a single metric - (only `type` and one other matching field should be set at once). - properties: - containerResource: - description: |- - containerResource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing a single container in - each pod of the current scale target (e.g. CPU or memory). Such metrics are - built in to Kubernetes, and have special scaling options on top of those - available to normal per-pod metrics using the "pods" source. - properties: - container: - description: container is the name of the container in the pods of the scaling target - type: string - name: - description: name is the name of the resource in question. - type: string - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - container - - name - - target - type: object - external: - description: |- - external refers to a global metric that is not associated - with any Kubernetes object. It allows autoscaling based on information - coming from components running outside of cluster - (for example length of queue in cloud messaging service, or - QPS from loadbalancer running outside of cluster). - properties: - metric: - description: metric identifies the target metric by name and selector - properties: - name: - description: name is the name of the given metric - type: string - selector: - description: |- - selector is the string-encoded form of a standard kubernetes label selector for the given metric - When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. - When unset, just the metricName will be used to gather metrics. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - metric - - target - type: object - object: - description: |- - object refers to a metric describing a single kubernetes object - (for example, hits-per-second on an Ingress object). - properties: - describedObject: - description: describedObject specifies the descriptions of a object,such as kind,name apiVersion - properties: - apiVersion: - description: apiVersion is the API version of the referent - type: string - kind: - description: 'kind is the kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - name: - description: 'name is the name of the referent; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - required: - - kind - - name - type: object - metric: - description: metric identifies the target metric by name and selector - properties: - name: - description: name is the name of the given metric - type: string - selector: - description: |- - selector is the string-encoded form of a standard kubernetes label selector for the given metric - When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. - When unset, just the metricName will be used to gather metrics. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - describedObject - - metric - - target - type: object - pods: - description: |- - pods refers to a metric describing each pod in the current scale target - (for example, transactions-processed-per-second). The values will be - averaged together before being compared to the target value. - properties: - metric: - description: metric identifies the target metric by name and selector - properties: - name: - description: name is the name of the given metric - type: string - selector: - description: |- - selector is the string-encoded form of a standard kubernetes label selector for the given metric - When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. - When unset, just the metricName will be used to gather metrics. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - metric - - target - type: object - resource: - description: |- - resource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing each pod in the - current scale target (e.g. CPU or memory). Such metrics are built in to - Kubernetes, and have special scaling options on top of those available - to normal per-pod metrics using the "pods" source. - properties: - name: - description: name is the name of the resource in question. - type: string - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - name - - target - type: object - type: - description: |- - type is the type of metric source. It should be one of "ContainerResource", "External", - "Object", "Pods" or "Resource", each mapping to a matching field in the object. - type: string - required: - - type - type: object - type: array - minReplicas: - format: int32 - type: integer - type: object - ingressRouteUrls: - description: |- - Optional list of URLs where the service can be reached - By default only the spec.service.url is used - items: - properties: - url: - pattern: ^https?://.+/.+ - type: string - required: - - url - type: object - maxItems: 30 - minItems: 1 - type: array - lifecycle: - description: Optional lifecycle settings - properties: - ttlInDays: - format: int32 - type: integer - type: object - options: - description: Options configures optional behaviors of the operator, like ingress, casing, and data prefetching. - properties: - automaticCasing: - default: true - description: AutomaticCasing enables automatic conversion from snake_case to camelCase. - type: boolean - includeIngress: - default: true - description: IncludeIngress dictates whether to deploy an Ingress or ensure none exists. - type: boolean - prefetchData: - default: true - description: |- - Whether to prefetch data from blob storage, and store it on the local filesystem. - If `false`, the data will be served directly out of blob storage - type: boolean - type: object - podSpecPatch: - description: Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. - type: object - x-kubernetes-preserve-unknown-fields: true - service: - description: service configuration - properties: - abstract: - description: Service abstract - minLength: 1 - type: string - accessConstraints: - default: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - description: AccessConstraints URL - pattern: ^https?://.+/.+ - type: string - bbox: - description: Service bounding box - properties: - defaultCRS: - description: EXTENT/wfs_extent in mapfile - properties: - maxx: - description: Rechtsonder X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - maxy: - description: Rechtsonder Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - minx: - description: Linksboven X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - miny: - description: Linksboven Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - required: - - maxx - - maxy - - minx - - miny - type: object - required: - - defaultCRS - type: object - countDefault: - description: CountDefault -> wfs_maxfeatures in mapfile - minimum: 1 - type: integer - defaultCrs: - description: Default CRS (DataEPSG) - pattern: ^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$ - type: string - featureTypes: - description: FeatureTypes configurations - items: - description: FeatureType defines a WFS feature - properties: - abstract: - description: Abstract of the feature - minLength: 1 - type: string - bbox: - description: Optional feature bbox - properties: - defaultCRS: - description: DefaultCRS defines the EXTENT/wfs_extent for the featureType for use in the mapfile - properties: - maxx: - description: Rechtsonder X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - maxy: - description: Rechtsonder Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - minx: - description: Linksboven X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - miny: - description: Linksboven Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - required: - - maxx - - maxy - - minx - - miny - type: object - wgs84: - description: WGS84, if provided, gives the same bounding box reprojected into EPSG:4326 for use in the capabilities. - properties: - maxx: - description: Rechtsonder X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - maxy: - description: Rechtsonder Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - minx: - description: Linksboven X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - miny: - description: Linksboven Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - required: - - maxx - - maxy - - minx - - miny - type: object - type: object - data: - description: FeatureType data connection - properties: - gpkg: - description: Gpkg configures a GeoPackage file source - properties: - blobKey: - description: Blobkey identifies the location/bucket of the .gpkg file - pattern: ^.+\/.+\/.+\.gpkg$ - type: string - columns: - description: Columns to visualize for this table - items: - description: Column maps a source column name to an optional alias for output. - properties: - alias: - description: Alias for the column in the service output. - minLength: 1 - type: string - name: - description: Name of the column in the data source. - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - geometryType: - description: GeometryType of the table, must match an OGC type - pattern: ^(Multi)?(Point|LineString|Polygon)$ - type: string - tableName: - description: TableName is the table within the geopackage - minLength: 1 - type: string - required: - - blobKey - - columns - - geometryType - - tableName - type: object - postgis: - description: Postgis configures a Postgis table source - properties: - columns: - description: Columns to expose from table - items: - description: Column maps a source column name to an optional alias for output. - properties: - alias: - description: Alias for the column in the service output. - minLength: 1 - type: string - name: - description: Name of the column in the data source. - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - geometryType: - description: GeometryType of the table - pattern: ^(Multi)?(Point|LineString|Polygon)$ - type: string - tableName: - description: TableName in postGIS - minLength: 1 - type: string - required: - - columns - - geometryType - - tableName - type: object - type: object - x-kubernetes-validations: - - message: At least one of the datasource should be provided (postgis, gpkg) - rule: has(self.gpkg) || has(self.postgis) - datasetMetadataUrl: - description: Metadata URL - properties: - csw: - description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. - properties: - metadataIdentifier: - description: MetadataIdentifier is the record's UUID - pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ - type: string - required: - - metadataIdentifier - type: object - custom: - description: Custom allows arbitrary href - properties: - href: - description: Href of the custom metadata url - pattern: ^https?://.+/.+ - type: string - type: - description: MIME type of the custom link - minLength: 1 - type: string - required: - - href - - type - type: object - type: object - x-kubernetes-validations: - - message: metadataUrl should have exactly 1 of csw or custom - rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) - keywords: - description: Keywords of the feature - items: - minLength: 1 - type: string - minItems: 1 - type: array - name: - description: Name of the feature - pattern: ^\S+$ - type: string - title: - description: Title of the feature - minLength: 1 - type: string - required: - - abstract - - data - - keywords - - name - - title - type: object - minItems: 1 - type: array - fees: - description: Optional Fees - minLength: 1 - type: string - inspire: - description: Inspire holds INSPIRE-specific metadata for the service. - properties: - language: - description: Language of the INSPIRE metadata record - pattern: bul|cze|dan|dut|eng|est|fin|fre|ger|gre|hun|gle|ita|lav|lit|mlt|pol|por|rum|slo|slv|spa|swe - type: string - serviceMetadataUrl: - description: ServiceMetadataURL references the CSW or custom metadata record. - properties: - csw: - description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. - properties: - metadataIdentifier: - description: MetadataIdentifier is the record's UUID - pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ - type: string - required: - - metadataIdentifier - type: object - custom: - description: Custom allows arbitrary href - properties: - href: - description: Href of the custom metadata url - pattern: ^https?://.+/.+ - type: string - type: - description: MIME type of the custom link - minLength: 1 - type: string - required: - - href - - type - type: object - type: object - x-kubernetes-validations: - - message: metadataUrl should have exactly 1 of csw or custom - rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) - spatialDatasetIdentifier: - description: SpatialDatasetIdentifier is the ID uniquely identifying the dataset. - pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ - type: string - required: - - language - - serviceMetadataUrl - - spatialDatasetIdentifier - type: object - keywords: - description: Keywords for capabilities - items: - minLength: 1 - type: string - minItems: 1 - type: array - mapfile: - description: External Mapfile reference - properties: - configMapKeyRef: - description: Selects a key from a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: Name of the referent. - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - - name - type: object - x-kubernetes-map-type: atomic - required: - - configMapKeyRef - type: object - otherCrs: - description: Other supported CRS - items: - pattern: ^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$ - type: string - minItems: 1 - type: array - ownerInfoRef: - description: Reference to OwnerInfo CR - minLength: 1 - type: string - prefix: - description: Geonovum subdomein - minLength: 1 - type: string - title: - description: Service title - minLength: 1 - type: string - url: - description: URL of the service - pattern: ^https?://.+/.+ - type: string - required: - - abstract - - defaultCrs - - featureTypes - - keywords - - ownerInfoRef - - prefix - - title - - url - type: object - x-kubernetes-validations: - - fieldPath: .otherCrs - message: otherCrs can't contain the defaultCrs - rule: '!has(self.otherCrs) || (has(self.otherCrs) && !(self.defaultCrs in self.otherCrs))' - required: - - podSpecPatch - - service - type: object - x-kubernetes-validations: - - messageExpression: '''ingressRouteUrls should include service.url ''+self.service.url' - rule: '!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)' - status: - description: OperatorStatus defines the observed state of an Atom/WFS/WMS/OGCAPI/... - properties: - conditions: - description: |- - Each condition contains details for one aspect of the current state of this CR. - Known .status.conditions.type are: "Reconciled" - items: - description: Condition contains details for one aspect of the current state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - operationResults: - additionalProperties: - description: OperationResult is the action result of a CreateOrUpdate or CreateOrPatch call. - type: string - description: The result of creating or updating of each derived resource for this CR. - type: object - podSummary: - description: Summary of status of pods that belong to this CR - items: - properties: - available: - format: int32 - type: integer - generation: - format: int32 - type: integer - ready: - format: int32 - type: integer - total: - format: int32 - type: integer - unavailable: - format: int32 - type: integer - required: - - available - - generation - - ready - - total - - unavailable - type: object - type: array - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/pdok.nl_wfses.yaml b/config/crd/bases/pdok.nl_wfses.yaml new file mode 100644 index 0000000..4d8ef84 --- /dev/null +++ b/config/crd/bases/pdok.nl_wfses.yaml @@ -0,0 +1,92 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: wfses.pdok.nl +spec: + group: pdok.nl + names: + kind: WFS + listKind: WFSList + plural: wfses + singular: wfs + scope: Namespaced + versions: + - name: v2beta1 + schema: + openAPIV3Schema: + description: WFS is the Schema for the wfs API. + 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: WFSSpec defines the desired state of WFS. + properties: + foo: + description: Foo is an example field of WFS. Edit wfs_types.go to + remove/update + type: string + type: object + status: + description: WFSStatus defines the observed state of WFS. + type: object + type: object + served: true + storage: false + subresources: + status: {} + - name: v3 + schema: + openAPIV3Schema: + description: WFS is the Schema for the wfs API. + 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: WFSSpec defines the desired state of WFS. + properties: + foo: + description: Foo is an example field of WFS. Edit wfs_types.go to + remove/update + type: string + type: object + status: + description: WFSStatus defines the observed state of WFS. + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/pdok.nl_wms.yaml b/config/crd/bases/pdok.nl_wms.yaml deleted file mode 100644 index 35ed9aa..0000000 --- a/config/crd/bases/pdok.nl_wms.yaml +++ /dev/null @@ -1,1782 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.17.1 - creationTimestamp: null - name: wms.pdok.nl -spec: - group: pdok.nl - names: - categories: - - pdok - kind: WMS - listKind: WMSList - plural: wms - singular: wms - scope: Namespaced - versions: - - additionalPrinterColumns: - - jsonPath: .status.podSummary[0].ready - name: ReadyPods - type: integer - - jsonPath: .status.podSummary[0].total - name: DesiredPods - type: integer - - jsonPath: .status.conditions[?(@.type == "Reconciled")].reason - name: ReconcileStatus - type: string - name: v3 - schema: - openAPIV3Schema: - description: WMS is the Schema for the wms API. - 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: WMSSpec defines the desired state of WMS. - properties: - healthCheck: - description: Custom healthcheck options - properties: - boundingbox: - description: BBox defines a bounding box with coordinates - properties: - maxx: - description: Rechtsonder X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - maxy: - description: Rechtsonder Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - minx: - description: Linksboven X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - miny: - description: Linksboven Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - required: - - maxx - - maxy - - minx - - miny - type: object - mimetype: - pattern: (image/png|text/xml|text/html) - type: string - querystring: - type: string - x-kubernetes-validations: - - message: a valid healthcheck contains 'SERVICE=WMS' - rule: self.lowerAscii().contains('service=wms') - - message: a valid healthcheck contains 'REQUEST=' - rule: self.lowerAscii().contains('request=') - type: object - x-kubernetes-validations: - - message: mimetype is required when a querystring is used - rule: '!has(self.querystring) || has(self.mimetype)' - - message: healthcheck should have exactly 1 of querystring + mimetype or boundingbox - rule: (has(self.boundingbox) || has(self.querystring)) && !(has(self.querystring) && has(self.boundingbox)) - - message: healthcheck should have exactly 1 of querystring + mimetype or boundingbox - rule: (has(self.boundingbox) || has(self.mimetype)) && !(has(self.mimetype) && has(self.boundingbox)) - horizontalPodAutoscalerPatch: - description: Optional specification for the HorizontalAutoscaler - properties: - behavior: - description: |- - HorizontalPodAutoscalerBehavior configures the scaling behavior of the target - in both Up and Down directions (scaleUp and scaleDown fields respectively). - properties: - scaleDown: - description: |- - scaleDown is scaling policy for scaling Down. - If not set, the default value is to allow to scale down to minReplicas pods, with a - 300 second stabilization window (i.e., the highest recommendation for - the last 300sec is used). - properties: - policies: - description: |- - policies is a list of potential scaling polices which can be used during scaling. - If not set, use the default values: - - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. - - For scale down: allow all pods to be removed in a 15s window. - items: - description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. - properties: - periodSeconds: - description: |- - periodSeconds specifies the window of time for which the policy should hold true. - PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). - format: int32 - type: integer - type: - description: type is used to specify the scaling policy. - type: string - value: - description: |- - value contains the amount of change which is permitted by the policy. - It must be greater than zero - format: int32 - type: integer - required: - - periodSeconds - - type - - value - type: object - type: array - x-kubernetes-list-type: atomic - selectPolicy: - description: |- - selectPolicy is used to specify which policy should be used. - If not set, the default value Max is used. - type: string - stabilizationWindowSeconds: - description: |- - stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up or scaling down. - StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). - If not set, use the default values: - - For scale up: 0 (i.e. no stabilization is done). - - For scale down: 300 (i.e. the stabilization window is 300 seconds long). - format: int32 - type: integer - tolerance: - anyOf: - - type: integer - - type: string - description: |- - tolerance is the tolerance on the ratio between the current and desired - metric value under which no updates are made to the desired number of - replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not - set, the default cluster-wide tolerance is applied (by default 10%). - - For example, if autoscaling is configured with a memory consumption target of 100Mi, - and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be - triggered when the actual consumption falls below 95Mi or exceeds 101Mi. - - This is an alpha field and requires enabling the HPAConfigurableTolerance - feature gate. - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - scaleUp: - description: |- - scaleUp is scaling policy for scaling Up. - If not set, the default value is the higher of: - * increase no more than 4 pods per 60 seconds - * double the number of pods per 60 seconds - No stabilization is used. - properties: - policies: - description: |- - policies is a list of potential scaling polices which can be used during scaling. - If not set, use the default values: - - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. - - For scale down: allow all pods to be removed in a 15s window. - items: - description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. - properties: - periodSeconds: - description: |- - periodSeconds specifies the window of time for which the policy should hold true. - PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). - format: int32 - type: integer - type: - description: type is used to specify the scaling policy. - type: string - value: - description: |- - value contains the amount of change which is permitted by the policy. - It must be greater than zero - format: int32 - type: integer - required: - - periodSeconds - - type - - value - type: object - type: array - x-kubernetes-list-type: atomic - selectPolicy: - description: |- - selectPolicy is used to specify which policy should be used. - If not set, the default value Max is used. - type: string - stabilizationWindowSeconds: - description: |- - stabilizationWindowSeconds is the number of seconds for which past recommendations should be - considered while scaling up or scaling down. - StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). - If not set, use the default values: - - For scale up: 0 (i.e. no stabilization is done). - - For scale down: 300 (i.e. the stabilization window is 300 seconds long). - format: int32 - type: integer - tolerance: - anyOf: - - type: integer - - type: string - description: |- - tolerance is the tolerance on the ratio between the current and desired - metric value under which no updates are made to the desired number of - replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not - set, the default cluster-wide tolerance is applied (by default 10%). - - For example, if autoscaling is configured with a memory consumption target of 100Mi, - and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be - triggered when the actual consumption falls below 95Mi or exceeds 101Mi. - - This is an alpha field and requires enabling the HPAConfigurableTolerance - feature gate. - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - type: object - maxReplicas: - format: int32 - type: integer - metrics: - items: - description: |- - MetricSpec specifies how to scale based on a single metric - (only `type` and one other matching field should be set at once). - properties: - containerResource: - description: |- - containerResource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing a single container in - each pod of the current scale target (e.g. CPU or memory). Such metrics are - built in to Kubernetes, and have special scaling options on top of those - available to normal per-pod metrics using the "pods" source. - properties: - container: - description: container is the name of the container in the pods of the scaling target - type: string - name: - description: name is the name of the resource in question. - type: string - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - container - - name - - target - type: object - external: - description: |- - external refers to a global metric that is not associated - with any Kubernetes object. It allows autoscaling based on information - coming from components running outside of cluster - (for example length of queue in cloud messaging service, or - QPS from loadbalancer running outside of cluster). - properties: - metric: - description: metric identifies the target metric by name and selector - properties: - name: - description: name is the name of the given metric - type: string - selector: - description: |- - selector is the string-encoded form of a standard kubernetes label selector for the given metric - When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. - When unset, just the metricName will be used to gather metrics. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - metric - - target - type: object - object: - description: |- - object refers to a metric describing a single kubernetes object - (for example, hits-per-second on an Ingress object). - properties: - describedObject: - description: describedObject specifies the descriptions of a object,such as kind,name apiVersion - properties: - apiVersion: - description: apiVersion is the API version of the referent - type: string - kind: - description: 'kind is the kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - name: - description: 'name is the name of the referent; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - required: - - kind - - name - type: object - metric: - description: metric identifies the target metric by name and selector - properties: - name: - description: name is the name of the given metric - type: string - selector: - description: |- - selector is the string-encoded form of a standard kubernetes label selector for the given metric - When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. - When unset, just the metricName will be used to gather metrics. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - describedObject - - metric - - target - type: object - pods: - description: |- - pods refers to a metric describing each pod in the current scale target - (for example, transactions-processed-per-second). The values will be - averaged together before being compared to the target value. - properties: - metric: - description: metric identifies the target metric by name and selector - properties: - name: - description: name is the name of the given metric - type: string - selector: - description: |- - selector is the string-encoded form of a standard kubernetes label selector for the given metric - When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. - When unset, just the metricName will be used to gather metrics. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: |- - A label selector requirement is a selector that contains values, a key, and an operator that - relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: |- - operator represents a key's relationship to a set of values. - Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: |- - values is an array of string values. If the operator is In or NotIn, - the values array must be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced during a strategic - merge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: |- - matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, whose key field is "key", the - operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - required: - - name - type: object - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - metric - - target - type: object - resource: - description: |- - resource refers to a resource metric (such as those specified in - requests and limits) known to Kubernetes describing each pod in the - current scale target (e.g. CPU or memory). Such metrics are built in to - Kubernetes, and have special scaling options on top of those available - to normal per-pod metrics using the "pods" source. - properties: - name: - description: name is the name of the resource in question. - type: string - target: - description: target specifies the target value for the given metric - properties: - averageUtilization: - description: |- - averageUtilization is the target value of the average of the - resource metric across all relevant pods, represented as a percentage of - the requested value of the resource for the pods. - Currently only valid for Resource metric source type - format: int32 - type: integer - averageValue: - anyOf: - - type: integer - - type: string - description: |- - averageValue is the target value of the average of the - metric across all relevant pods (as a quantity) - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: - description: type represents whether the metric type is Utilization, Value, or AverageValue - type: string - value: - anyOf: - - type: integer - - type: string - description: value is the target value of the metric (as a quantity). - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - type - type: object - required: - - name - - target - type: object - type: - description: |- - type is the type of metric source. It should be one of "ContainerResource", "External", - "Object", "Pods" or "Resource", each mapping to a matching field in the object. - type: string - required: - - type - type: object - type: array - minReplicas: - format: int32 - type: integer - type: object - ingressRouteUrls: - description: |- - Optional list of URLs where the service can be reached - By default only the spec.service.url is used - items: - properties: - url: - pattern: ^https?://.+/.+ - type: string - required: - - url - type: object - maxItems: 30 - minItems: 1 - type: array - lifecycle: - description: Optional lifecycle settings - properties: - ttlInDays: - format: int32 - type: integer - type: object - options: - description: Optional options for the configuration of the service. - properties: - automaticCasing: - default: true - description: AutomaticCasing enables automatic conversion from snake_case to camelCase. - type: boolean - disableWebserviceProxy: - default: false - description: DisableWebserviceProxy disables the built-in proxy for external web services. - type: boolean - includeIngress: - default: true - description: IncludeIngress dictates whether to deploy an Ingress or ensure none exists. - type: boolean - prefetchData: - default: true - description: |- - Whether to prefetch data from blob storage, and store it on the local filesystem. - If `false`, the data will be served directly out of blob storage - type: boolean - rewriteGroupToDataLayers: - default: false - description: RewriteGroupToDataLayers merges group layers into individual data layers. - type: boolean - validateChildStyleNameEqual: - default: false - description: ValidateChildStyleNameEqual ensures child style names match the parent style. - type: boolean - validateRequests: - default: true - description: ValidateRequests enables request validation against the service schema. - type: boolean - type: object - podSpecPatch: - description: Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. - type: object - x-kubernetes-preserve-unknown-fields: true - service: - description: Service specification - properties: - abstract: - description: Service abstract - minLength: 1 - type: string - accessConstraints: - default: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - description: AccessConstraints URL - pattern: ^https?://.+/.+ - type: string - dataEPSG: - description: CRS of the data - pattern: (EPSG|CRS):\d+ - type: string - defResolution: - description: 'Mapfile setting: Sets the DEFRESOLUTION field in the mapfile, not used when service.mapfile is configured' - format: int32 - type: integer - fees: - description: Optional Fees - minLength: 1 - type: string - inspire: - description: Config for Inspire services - properties: - language: - description: Language of the INSPIRE metadata record - pattern: bul|cze|dan|dut|eng|est|fin|fre|ger|gre|hun|gle|ita|lav|lit|mlt|pol|por|rum|slo|slv|spa|swe - type: string - serviceMetadataUrl: - description: ServiceMetadataURL references the CSW or custom metadata record. - properties: - csw: - description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. - properties: - metadataIdentifier: - description: MetadataIdentifier is the record's UUID - pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ - type: string - required: - - metadataIdentifier - type: object - custom: - description: Custom allows arbitrary href - properties: - href: - description: Href of the custom metadata url - pattern: ^https?://.+/.+ - type: string - type: - description: MIME type of the custom link - minLength: 1 - type: string - required: - - href - - type - type: object - type: object - x-kubernetes-validations: - - message: metadataUrl should have exactly 1 of csw or custom - rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) - required: - - language - - serviceMetadataUrl - type: object - keywords: - description: Keywords for capabilities - items: - minLength: 1 - type: string - minItems: 1 - type: array - layer: - description: Toplayer - properties: - abstract: - description: Abstract of the layer - minLength: 1 - type: string - authority: - properties: - name: - type: string - spatialDatasetIdentifier: - type: string - url: - type: string - required: - - name - - spatialDatasetIdentifier - - url - type: object - boundingBoxes: - description: BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. - items: - properties: - bbox: - description: BBox defines a bounding box with coordinates - properties: - maxx: - description: Rechtsonder X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - maxy: - description: Rechtsonder Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - minx: - description: Linksboven X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - miny: - description: Linksboven Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - required: - - maxx - - maxy - - minx - - miny - type: object - crs: - pattern: ^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$ - type: string - required: - - bbox - - crs - type: object - minItems: 1 - type: array - datasetMetadataUrl: - description: Links to metadata - properties: - csw: - description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. - properties: - metadataIdentifier: - description: MetadataIdentifier is the record's UUID - pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ - type: string - required: - - metadataIdentifier - type: object - custom: - description: Custom allows arbitrary href - properties: - href: - description: Href of the custom metadata url - pattern: ^https?://.+/.+ - type: string - type: - description: MIME type of the custom link - minLength: 1 - type: string - required: - - href - - type - type: object - type: object - x-kubernetes-validations: - - message: metadataUrl should have exactly 1 of csw or custom - rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) - keywords: - description: Keywords of the layer, required if the layer is visible - items: - minLength: 1 - type: string - minItems: 1 - type: array - layers: - description: '[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]' - items: - description: Toplayer - properties: - abstract: - description: Abstract of the layer - minLength: 1 - type: string - authority: - properties: - name: - type: string - spatialDatasetIdentifier: - type: string - url: - type: string - required: - - name - - spatialDatasetIdentifier - - url - type: object - boundingBoxes: - description: BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. - items: - properties: - bbox: - description: BBox defines a bounding box with coordinates - properties: - maxx: - description: Rechtsonder X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - maxy: - description: Rechtsonder Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - minx: - description: Linksboven X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - miny: - description: Linksboven Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - required: - - maxx - - maxy - - minx - - miny - type: object - crs: - pattern: ^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$ - type: string - required: - - bbox - - crs - type: object - minItems: 1 - type: array - data: - description: Data (gpkg/postgis/tif) used by the layer - properties: - gpkg: - description: Gpkg configures a GeoPackage file source - properties: - blobKey: - description: Blobkey identifies the location/bucket of the .gpkg file - pattern: ^.+\/.+\/.+\.gpkg$ - type: string - columns: - description: Columns to visualize for this table - items: - description: Column maps a source column name to an optional alias for output. - properties: - alias: - description: Alias for the column in the service output. - minLength: 1 - type: string - name: - description: Name of the column in the data source. - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - geometryType: - description: GeometryType of the table, must match an OGC type - pattern: ^(Multi)?(Point|LineString|Polygon)$ - type: string - tableName: - description: TableName is the table within the geopackage - minLength: 1 - type: string - required: - - blobKey - - columns - - geometryType - - tableName - type: object - postgis: - description: Postgis configures a Postgis table source - properties: - columns: - description: Columns to expose from table - items: - description: Column maps a source column name to an optional alias for output. - properties: - alias: - description: Alias for the column in the service output. - minLength: 1 - type: string - name: - description: Name of the column in the data source. - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - geometryType: - description: GeometryType of the table - pattern: ^(Multi)?(Point|LineString|Polygon)$ - type: string - tableName: - description: TableName in postGIS - minLength: 1 - type: string - required: - - columns - - geometryType - - tableName - type: object - tif: - description: TIF configures a GeoTIF raster source - properties: - blobKey: - description: BlobKey to the TIFF file - pattern: ^.+\/.+\/.+\.(tif?f|vrt)$ - type: string - getFeatureInfoIncludesClass: - default: false - description: '"When a band represents nominal or ordinal data the class name (from styling) can be included in the getFeatureInfo"' - type: boolean - offsite: - description: Sets the color index to treat as transparent for raster layers, optional, hex or rgb - pattern: (#[0-9A-F]{6}([0-9A-F]{2})?)|([0-9]{1,3}\s[0-9]{1,3}\s[0-9]{1,3}) - type: string - oversampleRatio: - default: "2.5" - description: |- - Controls the smoothing of the image on a certain point. Bigger value gives a smoother/better picture but - results in slower web responses, optional - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - resample: - default: NEAREST - description: This option can be used to control the resampling kernel used sampling raster images, optional - pattern: (NEAREST|AVERAGE|BILINEAR) - type: string - required: - - blobKey - type: object - type: object - x-kubernetes-validations: - - message: Atleast one of the datasource should be provided (postgis, gpkg, tif) - rule: has(self.gpkg) || has(self.tif) || has(self.postgis) - datasetMetadataUrl: - description: Links to metadata - properties: - csw: - description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. - properties: - metadataIdentifier: - description: MetadataIdentifier is the record's UUID - pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ - type: string - required: - - metadataIdentifier - type: object - custom: - description: Custom allows arbitrary href - properties: - href: - description: Href of the custom metadata url - pattern: ^https?://.+/.+ - type: string - type: - description: MIME type of the custom link - minLength: 1 - type: string - required: - - href - - type - type: object - type: object - x-kubernetes-validations: - - message: metadataUrl should have exactly 1 of csw or custom - rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) - keywords: - description: Keywords of the layer, required if the layer is visible - items: - minLength: 1 - type: string - minItems: 1 - type: array - labelNoClip: - description: Mapfile setting, sets "LABEL_NO_CLIP=ON" - type: boolean - layers: - description: '[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]' - items: - description: Toplayer - properties: - abstract: - description: Abstract of the layer - minLength: 1 - type: string - authority: - properties: - name: - type: string - spatialDatasetIdentifier: - type: string - url: - type: string - required: - - name - - spatialDatasetIdentifier - - url - type: object - boundingBoxes: - description: BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. - items: - properties: - bbox: - description: BBox defines a bounding box with coordinates - properties: - maxx: - description: Rechtsonder X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - maxy: - description: Rechtsonder Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - minx: - description: Linksboven X coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - miny: - description: Linksboven Y coรถrdinaat - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - required: - - maxx - - maxy - - minx - - miny - type: object - crs: - pattern: ^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$ - type: string - required: - - bbox - - crs - type: object - minItems: 1 - type: array - data: - description: Data (gpkg/postgis/tif) used by the layer - properties: - gpkg: - description: Gpkg configures a GeoPackage file source - properties: - blobKey: - description: Blobkey identifies the location/bucket of the .gpkg file - pattern: ^.+\/.+\/.+\.gpkg$ - type: string - columns: - description: Columns to visualize for this table - items: - description: Column maps a source column name to an optional alias for output. - properties: - alias: - description: Alias for the column in the service output. - minLength: 1 - type: string - name: - description: Name of the column in the data source. - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - geometryType: - description: GeometryType of the table, must match an OGC type - pattern: ^(Multi)?(Point|LineString|Polygon)$ - type: string - tableName: - description: TableName is the table within the geopackage - minLength: 1 - type: string - required: - - blobKey - - columns - - geometryType - - tableName - type: object - postgis: - description: Postgis configures a Postgis table source - properties: - columns: - description: Columns to expose from table - items: - description: Column maps a source column name to an optional alias for output. - properties: - alias: - description: Alias for the column in the service output. - minLength: 1 - type: string - name: - description: Name of the column in the data source. - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - geometryType: - description: GeometryType of the table - pattern: ^(Multi)?(Point|LineString|Polygon)$ - type: string - tableName: - description: TableName in postGIS - minLength: 1 - type: string - required: - - columns - - geometryType - - tableName - type: object - tif: - description: TIF configures a GeoTIF raster source - properties: - blobKey: - description: BlobKey to the TIFF file - pattern: ^.+\/.+\/.+\.(tif?f|vrt)$ - type: string - getFeatureInfoIncludesClass: - default: false - description: '"When a band represents nominal or ordinal data the class name (from styling) can be included in the getFeatureInfo"' - type: boolean - offsite: - description: Sets the color index to treat as transparent for raster layers, optional, hex or rgb - pattern: (#[0-9A-F]{6}([0-9A-F]{2})?)|([0-9]{1,3}\s[0-9]{1,3}\s[0-9]{1,3}) - type: string - oversampleRatio: - default: "2.5" - description: |- - Controls the smoothing of the image on a certain point. Bigger value gives a smoother/better picture but - results in slower web responses, optional - pattern: ^-?[0-9]+([.][0-9]*)?$ - type: string - resample: - default: NEAREST - description: This option can be used to control the resampling kernel used sampling raster images, optional - pattern: (NEAREST|AVERAGE|BILINEAR) - type: string - required: - - blobKey - type: object - type: object - x-kubernetes-validations: - - message: Atleast one of the datasource should be provided (postgis, gpkg, tif) - rule: has(self.gpkg) || has(self.tif) || has(self.postgis) - datasetMetadataUrl: - description: Links to metadata - properties: - csw: - description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. - properties: - metadataIdentifier: - description: MetadataIdentifier is the record's UUID - pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ - type: string - required: - - metadataIdentifier - type: object - custom: - description: Custom allows arbitrary href - properties: - href: - description: Href of the custom metadata url - pattern: ^https?://.+/.+ - type: string - type: - description: MIME type of the custom link - minLength: 1 - type: string - required: - - href - - type - type: object - type: object - x-kubernetes-validations: - - message: metadataUrl should have exactly 1 of csw or custom - rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) - keywords: - description: Keywords of the layer, required if the layer is visible - items: - minLength: 1 - type: string - minItems: 1 - type: array - labelNoClip: - description: Mapfile setting, sets "LABEL_NO_CLIP=ON" - type: boolean - maxscaledenominator: - description: The maximum scale at which this layer functions - pattern: ^[1-9][0-9]*(.[0-9]+)?$ - type: string - minscaledenominator: - description: The minimum scale at which this layer functions - pattern: ^[0-9]+(.[0-9]+)?$ - type: string - name: - description: Name of the layer, required for layers on the 2nd or 3rd level - minLength: 1 - type: string - styles: - description: List of styles used by the layer - items: - properties: - abstract: - minLength: 1 - type: string - legend: - properties: - blobKey: - description: Location of the legend on the blobstore - minLength: 1 - type: string - format: - default: image/png - description: Format of the legend, defaults to image/png - type: string - height: - description: The height of the legend in px, defaults to 20 - format: int32 - type: integer - width: - description: The width of the legend in px, defaults to 78 - format: int32 - type: integer - required: - - blobKey - type: object - name: - minLength: 1 - type: string - title: - minLength: 1 - type: string - visualization: - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - title: - description: Title of the layer - minLength: 1 - type: string - visible: - default: true - description: Whether or not the layer is visible. At least one of the layers must be visible. - type: boolean - required: - - name - - styles - type: object - x-kubernetes-validations: - - message: A layer with data attribute should have styling - rule: '!has(self.data) || has(self.styles)' - - message: A layer should have a title when visible - rule: '!self.visible || has(self.title)' - - message: A layer should have an abstract when visible - rule: '!self.visible || has(self.abstract)' - - message: A layer should have keywords when visible - rule: '!self.visible || has(self.keywords)' - minItems: 1 - type: array - maxscaledenominator: - description: The maximum scale at which this layer functions - pattern: ^[1-9][0-9]*(.[0-9]+)?$ - type: string - minscaledenominator: - description: The minimum scale at which this layer functions - pattern: ^[0-9]+(.[0-9]+)?$ - type: string - name: - description: Name of the layer, required for layers on the 2nd or 3rd level - minLength: 1 - type: string - styles: - description: List of styles used by the layer - items: - properties: - abstract: - minLength: 1 - type: string - legend: - properties: - blobKey: - description: Location of the legend on the blobstore - minLength: 1 - type: string - format: - default: image/png - description: Format of the legend, defaults to image/png - type: string - height: - description: The height of the legend in px, defaults to 20 - format: int32 - type: integer - width: - description: The width of the legend in px, defaults to 78 - format: int32 - type: integer - required: - - blobKey - type: object - name: - minLength: 1 - type: string - title: - minLength: 1 - type: string - visualization: - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - title: - description: Title of the layer - minLength: 1 - type: string - visible: - default: true - description: Whether or not the layer is visible. At least one of the layers must be visible. - type: boolean - required: - - name - - styles - type: object - x-kubernetes-validations: - - message: A layer should have exactly one of sublayers or data - rule: (has(self.data) || has(self.layers)) && !(has(self.data) && has(self.layers)) - - message: A layer with data attribute should have styling - rule: '!has(self.data) || has(self.styles)' - - message: A layer should have a title when visible - rule: '!self.visible || has(self.title)' - - message: A layer should have an abstract when visible - rule: '!self.visible || has(self.abstract)' - - message: A layer should have keywords when visible - rule: '!self.visible || has(self.keywords)' - minItems: 1 - type: array - maxscaledenominator: - description: The maximum scale at which this layer functions - pattern: ^[1-9][0-9]*(.[0-9]+)?$ - type: string - minscaledenominator: - description: The minimum scale at which this layer functions - pattern: ^[0-9]+(.[0-9]+)?$ - type: string - name: - description: Name of the layer, required for layers on the 2nd or 3rd level - minLength: 1 - type: string - styles: - description: List of styles used by the layer - items: - properties: - abstract: - minLength: 1 - type: string - legend: - properties: - blobKey: - description: Location of the legend on the blobstore - minLength: 1 - type: string - format: - default: image/png - description: Format of the legend, defaults to image/png - type: string - height: - description: The height of the legend in px, defaults to 20 - format: int32 - type: integer - width: - description: The width of the legend in px, defaults to 78 - format: int32 - type: integer - required: - - blobKey - type: object - name: - minLength: 1 - type: string - title: - minLength: 1 - type: string - visualization: - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - title: - description: Title of the layer - minLength: 1 - type: string - visible: - default: true - description: Whether or not the layer is visible. At least one of the layers must be visible. - type: boolean - required: - - title - - abstract - - keywords - - boundingBoxes - - layers - type: object - x-kubernetes-validations: - - fieldPath: .visible - message: TopLayer must be visible - rule: self.visible - - fieldPath: .styles - message: If TopLayer has a name, it must have styles - rule: '!has(self.name) || has(self.styles)' - mapfile: - description: External Mapfile reference - properties: - configMapKeyRef: - description: Selects a key from a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - description: Name of the referent. - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - - name - type: object - x-kubernetes-map-type: atomic - required: - - configMapKeyRef - type: object - maxSize: - description: 'Mapfile setting: Sets the maximum size (in pixels) for both dimensions of the image from a getMap request.' - format: int32 - minimum: 1 - type: integer - ownerInfoRef: - description: Reference to OwnerInfo CR - minLength: 1 - type: string - prefix: - description: Geonovum subdomein - minLength: 1 - type: string - resolution: - description: 'Mapfile setting: Sets the RESOLUTION field in the mapfile, not used when service.mapfile is configured' - format: int32 - type: integer - stylingAssets: - description: Optional. Required files for the styling of the service - properties: - blobKeys: - description: 'BlobKeys contains symbol image (.png/.svg) or font (.ttf) keys on blob storage, format: container/key/file.(png|ttf)' - items: - pattern: ^.+\/.+\/.+\.(png|ttf|svg)$ - type: string - minItems: 1 - type: array - configMapRefs: - items: - properties: - keys: - description: Keys contains styling assets that contain mapfile code (.style|.symbol), required if you use symbols in your styles - items: - pattern: ^\S*.\.(style|symbol) - type: string - minItems: 1 - type: array - name: - description: Name is the name of the ConfigMap - minLength: 1 - type: string - required: - - name - type: object - minItems: 1 - type: array - type: object - x-kubernetes-validations: - - message: At least one of blobKeys or configMapRefs is required - rule: has(self.blobKeys) || has(self.configMapRefs) - title: - description: Service title - minLength: 1 - type: string - url: - description: URL of the service - pattern: ^https?://.+/.+ - type: string - required: - - abstract - - dataEPSG - - keywords - - layer - - ownerInfoRef - - prefix - - title - - url - type: object - x-kubernetes-validations: - - message: service requires styling, either through service.mapfile, or stylingAssets.configMapRefs - rule: has(self.mapfile) || (has(self.stylingAssets) && has(self.stylingAssets.configMapRefs)) - - message: when using service.mapfile, don't include stylingAssets.configMapRefs - rule: '!has(self.mapfile) || (!has(self.stylingAssets) || !has(self.stylingAssets.configMapRefs))' - required: - - podSpecPatch - - service - type: object - x-kubernetes-validations: - - messageExpression: '''ingressRouteUrls should include service.url ''+self.service.url' - rule: '!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)' - status: - description: OperatorStatus defines the observed state of an Atom/WFS/WMS/OGCAPI/... - properties: - conditions: - description: |- - Each condition contains details for one aspect of the current state of this CR. - Known .status.conditions.type are: "Reconciled" - items: - description: Condition contains details for one aspect of the current state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - operationResults: - additionalProperties: - description: OperationResult is the action result of a CreateOrUpdate or CreateOrPatch call. - type: string - description: The result of creating or updating of each derived resource for this CR. - type: object - podSummary: - description: Summary of status of pods that belong to this CR - items: - properties: - available: - format: int32 - type: integer - generation: - format: int32 - type: integer - ready: - format: int32 - type: integer - total: - format: int32 - type: integer - unavailable: - format: int32 - type: integer - required: - - available - - generation - - ready - - total - - unavailable - type: object - type: array - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/pdok.nl_wmses.yaml b/config/crd/bases/pdok.nl_wmses.yaml new file mode 100644 index 0000000..7d3ebcb --- /dev/null +++ b/config/crd/bases/pdok.nl_wmses.yaml @@ -0,0 +1,92 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.2 + name: wmses.pdok.nl +spec: + group: pdok.nl + names: + kind: WMS + listKind: WMSList + plural: wmses + singular: wms + scope: Namespaced + versions: + - name: v2beta1 + schema: + openAPIV3Schema: + description: WMS is the Schema for the wms API. + 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: WMSSpec defines the desired state of WMS. + properties: + foo: + description: Foo is an example field of WMS. Edit wms_types.go to + remove/update + type: string + type: object + status: + description: WMSStatus defines the observed state of WMS. + type: object + type: object + served: true + storage: false + subresources: + status: {} + - name: v3 + schema: + openAPIV3Schema: + description: WMS is the Schema for the wms API. + 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: WMSSpec defines the desired state of WMS. + properties: + foo: + description: Foo is an example field of WMS. Edit wms_types.go to + remove/update + type: string + type: object + status: + description: WMSStatus defines the observed state of WMS. + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 14028ae..92271d9 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,11 +6,11 @@ resources: - bases/pdok.nl_wfs.yaml # +kubebuilder:scaffold:crdkustomizeresource -# patches: +patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -# - path: patches/webhook_in_wfs.yaml -# - path: patches/webhook_in_wms.yaml +- path: patches/webhook_in_wms.yaml +- path: patches/webhook_in_wfs.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [WEBHOOK] To enable webhook, uncomment the following section diff --git a/config/crd/update_openapi.go b/config/crd/update_openapi.go deleted file mode 100644 index 0adbf1d..0000000 --- a/config/crd/update_openapi.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - goyaml "gopkg.in/yaml.v3" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - kyaml "sigs.k8s.io/yaml" -) - -// Usage: go run ./update_layersv3_openapi.go -func main() { - crdDir := os.Args[1] - - updateWMSV3(crdDir) - updateWFSV3(crdDir) -} - -func updateWMSV3(crdDir string) { - path := filepath.Join(crdDir, "pdok.nl_wms.yaml") - - if _, err := os.Stat(path); os.IsNotExist(err) { - panic(errors.Wrap(err, "WMS v3 manifest not found")) - } - - content, _ := os.ReadFile(path) - crd := &v1.CustomResourceDefinition{} - err := kyaml.Unmarshal(content, &crd) - if err != nil { - panic(err) - } - - versions := make([]v1.CustomResourceDefinitionVersion, 0) - for _, version := range crd.Spec.Versions { - if version.Name == "v3" { - updateMapfileV3(&version) - updateLayersV3(&version) - - versions = append(versions, version) - } else { - versions = append(versions, version) - } - } - - crd.Spec.Versions = versions - updatedContent, _ := kyaml.Marshal(crd) - - // Remove the 'status' field from the yaml - var rawData map[string]interface{} - _ = goyaml.Unmarshal(updatedContent, &rawData) - delete(rawData, "status") - - f, _ := os.OpenFile(path, os.O_TRUNC|os.O_WRONLY, 0644) - defer f.Close() - - enc := goyaml.NewEncoder(f) - defer enc.Close() - - enc.SetIndent(2) - _ = enc.Encode(rawData) -} - -func updateLayersV3(version *v1.CustomResourceDefinitionVersion) { - schema := version.Schema.OpenAPIV3Schema - spec := schema.Properties["spec"] - service := spec.Properties["service"] - layer := service.Properties["layer"] - - // Level 3 - layerSpecLevel3 := layer.DeepCopy() - layerSpecLevel3.Required = append(layerSpecLevel3.Required, "name") - layerSpecLevel3.Required = append(layerSpecLevel3.Required, "styles") - delete(layerSpecLevel3.Properties, "layers") - xvals := v1.ValidationRules{} - for _, xval := range layerSpecLevel3.XValidations { - if !strings.Contains(xval.Rule, "self.layers") { - xvals = append(xvals, xval) - } - } - layerSpecLevel3.XValidations = xvals - - // Level 2 - layerSpecLevel2 := layer.DeepCopy() - layerSpecLevel2.Required = append(layerSpecLevel2.Required, "name") - layerSpecLevel2.Required = append(layerSpecLevel2.Required, "styles") - bottomLayers := layerSpecLevel2.Properties["layers"] - bottomLayers.Description = "[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]" - bottomLayers.Items = &v1.JSONSchemaPropsOrArray{Schema: layerSpecLevel3} - layerSpecLevel2.Properties["layers"] = bottomLayers - - // Level 1 - layerSpecLevel1 := layer.DeepCopy() - layerSpecLevel1.Required = append(layerSpecLevel1.Required, "title", "abstract", "keywords", "boundingBoxes", "layers") - layerSpecLevel1.XValidations = []v1.ValidationRule{ - {Rule: "self.visible", Message: "TopLayer must be visible", FieldPath: ".visible"}, - {Rule: "!has(self.name) || has(self.styles)", Message: "If TopLayer has a name, it must have styles", FieldPath: ".styles"}, - } - delete(layerSpecLevel1.Properties, "data") - delete(layerSpecLevel1.Properties, "labelNoClip") - - midLayers := layerSpecLevel1.Properties["layers"] - midLayers.Description = "[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]" - midLayers.Items = &v1.JSONSchemaPropsOrArray{Schema: layerSpecLevel2} - - layerSpecLevel1.Properties["layers"] = midLayers - - service.Properties["layer"] = *layerSpecLevel1 - spec.Properties["service"] = service - schema.Properties["spec"] = spec - version.Schema = &v1.CustomResourceValidation{ - OpenAPIV3Schema: schema, - } -} - -func updateWFSV3(crdDir string) { - path := filepath.Join(crdDir, "pdok.nl_wfs.yaml") - - if _, err := os.Stat(path); os.IsNotExist(err) { - panic(errors.Wrap(err, "WFS v3 manifest not found")) - } - - content, _ := os.ReadFile(path) - crd := &v1.CustomResourceDefinition{} - err := kyaml.Unmarshal(content, &crd) - if err != nil { - panic(err) - } - - versions := make([]v1.CustomResourceDefinitionVersion, 0) - for _, version := range crd.Spec.Versions { - if version.Name == "v3" { - updateMapfileV3(&version) - - versions = append(versions, version) - } else { - versions = append(versions, version) - } - } - - crd.Spec.Versions = versions - updatedContent, _ := kyaml.Marshal(crd) - - // Remove the 'status' field from the yaml - var rawData map[string]interface{} - _ = goyaml.Unmarshal(updatedContent, &rawData) - delete(rawData, "status") - - f, _ := os.OpenFile(path, os.O_TRUNC|os.O_WRONLY, 0644) - defer f.Close() - - enc := goyaml.NewEncoder(f) - defer enc.Close() - - enc.SetIndent(2) - _ = enc.Encode(rawData) -} - -func updateMapfileV3(version *v1.CustomResourceDefinitionVersion) { - schema := version.Schema.OpenAPIV3Schema - spec := schema.Properties["spec"] - service := spec.Properties["service"] - mapfile := service.Properties["mapfile"] - configMapKeyRef := mapfile.Properties["configMapKeyRef"] - configMapKeyRef.Required = append(configMapKeyRef.Required, "name") - name := configMapKeyRef.Properties["name"] - name.Default = nil - name.Description = "Name of the referent." - - configMapKeyRef.Properties["name"] = name - mapfile.Properties["configMapKeyRef"] = configMapKeyRef - service.Properties["mapfile"] = mapfile - spec.Properties["service"] = service - schema.Properties["spec"] = spec - version.Schema = &v1.CustomResourceValidation{ - OpenAPIV3Schema: schema, - } -} diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 1306759..be72a73 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -22,7 +22,7 @@ resources: # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -- ../certmanager +#- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. @@ -37,9 +37,9 @@ resources: patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics -#- path: manager_metrics_patch.yaml -# target: -# kind: Deployment +- path: manager_metrics_patch.yaml + target: + kind: Deployment # Uncomment the patches line if you enable Metrics and CertManager # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. @@ -56,193 +56,215 @@ patches: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations -replacements: - - source: # Uncomment the following block to enable certificates for metrics - kind: Service - version: v1 - name: controller-manager-metrics-service - fieldPath: metadata.name - targets: - - select: - kind: Certificate - group: cert-manager.io - version: v1 - name: metrics-certs - fieldPaths: - - spec.dnsNames.0 - - spec.dnsNames.1 - options: - delimiter: '.' - index: 0 - create: true - - - source: - kind: Service - version: v1 - name: controller-manager-metrics-service - fieldPath: metadata.namespace - targets: - - select: - kind: Certificate - group: cert-manager.io - version: v1 - name: metrics-certs - fieldPaths: - - spec.dnsNames.0 - - spec.dnsNames.1 - options: - delimiter: '.' - index: 1 - create: true - - - source: # Uncomment the following block if you have any webhook - kind: Service - version: v1 - name: webhook-service - fieldPath: .metadata.name # Name of the service - targets: - - select: - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert - fieldPaths: - - .spec.dnsNames.0 - - .spec.dnsNames.1 - options: - delimiter: '.' - index: 0 - create: true - - source: - kind: Service - version: v1 - name: webhook-service - fieldPath: .metadata.namespace # Namespace of the service - targets: - - select: - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert - fieldPaths: - - .spec.dnsNames.0 - - .spec.dnsNames.1 - options: - delimiter: '.' - index: 1 - create: true - - - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert # This name should match the one in certificate.yaml - fieldPath: .metadata.namespace # Namespace of the certificate CR - targets: - - select: - kind: ValidatingWebhookConfiguration - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 0 - create: true - - source: - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert - fieldPath: .metadata.name - targets: - - select: - kind: ValidatingWebhookConfiguration - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 1 - create: true - - - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert - fieldPath: .metadata.namespace # Namespace of the certificate CR - targets: - - select: - kind: MutatingWebhookConfiguration - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 0 - create: true - - source: - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert - fieldPath: .metadata.name - targets: - - select: - kind: MutatingWebhookConfiguration - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 1 - create: true - - - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert - fieldPath: .metadata.namespace # Namespace of the certificate CR - targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. - - select: - kind: CustomResourceDefinition - name: wfs.pdok.nl - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 0 - create: true - - select: - kind: CustomResourceDefinition - name: wms.pdok.nl - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 0 - create: true +#replacements: +# - source: # Uncomment the following block to enable certificates for metrics +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.name +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 0 +# create: true +# +# - source: +# kind: Service +# version: v1 +# name: controller-manager-metrics-service +# fieldPath: metadata.namespace +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: metrics-certs +# fieldPaths: +# - spec.dnsNames.0 +# - spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true +# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor +# kind: ServiceMonitor +# group: monitoring.coreos.com +# version: v1 +# name: controller-manager-metrics-monitor +# fieldPaths: +# - spec.endpoints.0.tlsConfig.serverName +# options: +# delimiter: '.' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have any webhook +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # Name of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # Namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # This name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# +# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.namespace # Namespace of the certificate CR +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# - select: +# kind: CustomResourceDefinition +# name: wms.pdok.nl +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# name: wfs.pdok.nl +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true # +kubebuilder:scaffold:crdkustomizecainjectionns - - source: - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert - fieldPath: .metadata.name - targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. - - select: - kind: CustomResourceDefinition - name: wfs.pdok.nl - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 1 - create: true - - select: - kind: CustomResourceDefinition - name: wms.pdok.nl - fieldPaths: - - .metadata.annotations.[cert-manager.io/inject-ca-from] - options: - delimiter: '/' - index: 1 - create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert +# fieldPath: .metadata.name +# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. +# - select: +# kind: CustomResourceDefinition +# name: wms.pdok.nl +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# name: wfs.pdok.nl +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true # +kubebuilder:scaffold:crdkustomizecainjectionname diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index ebe5985..5c5f0b8 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,8 +1,2 @@ resources: - manager.yaml -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -images: -- name: controller - newName: local-registry:5000/mapserver-operator - newTag: v3.0.3 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 82e4d3f..2e94912 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -63,8 +63,6 @@ spec: args: - --leader-elect - --health-probe-bind-address=:8081 - - --baseurl=http://localhost:32788 - - --storage-class-name=test-storage image: controller:latest name: manager ports: [] diff --git a/config/prometheus/monitor_tls_patch.yaml b/config/prometheus/monitor_tls_patch.yaml index e824dd0..5bf84ce 100644 --- a/config/prometheus/monitor_tls_patch.yaml +++ b/config/prometheus/monitor_tls_patch.yaml @@ -1,22 +1,19 @@ # Patch for Prometheus ServiceMonitor to enable secure TLS configuration # using certificates managed by cert-manager -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: controller-manager-metrics-monitor - namespace: system -spec: - endpoints: - - tlsConfig: - insecureSkipVerify: false - ca: - secret: - name: metrics-server-cert - key: ca.crt - cert: - secret: - name: metrics-server-cert - key: tls.crt - keySecret: - name: metrics-server-cert - key: tls.key +- op: replace + path: /spec/endpoints/0/tlsConfig + value: + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize + serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc + insecureSkipVerify: false + ca: + secret: + name: metrics-server-cert + key: ca.crt + cert: + secret: + name: metrics-server-cert + key: tls.crt + keySecret: + name: metrics-server-cert + key: tls.key diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 64992be..c087f0f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,70 +4,6 @@ kind: ClusterRole metadata: name: manager-role rules: -- apiGroups: - - "" - resources: - - configmaps - - services - verbs: - - create - - delete - - get - - list - - update - - watch -- apiGroups: - - "" - resources: - - secrets - verbs: - - get - - list - - watch -- apiGroups: - - apps - resources: - - deployments - verbs: - - create - - delete - - get - - list - - update - - watch -- apiGroups: - - apps - resources: - - replicasets - verbs: - - get - - list - - watch -- apiGroups: - - autoscaling - resources: - - horizontalpodautoscalers - verbs: - - create - - delete - - get - - list - - update - - watch -- apiGroups: - - pdok.nl - resources: - - ownerinfo - verbs: - - get - - list - - watch -- apiGroups: - - pdok.nl - resources: - - ownerinfo/status - verbs: - - get - apiGroups: - pdok.nl resources: @@ -97,38 +33,3 @@ rules: - get - patch - update -- apiGroups: - - policy - resources: - - poddisruptionbudgets - verbs: - - create - - delete - - list - - update - - watch -- apiGroups: - - policy - resources: - - poddisruptionbudgets/finalizers - verbs: - - update -- apiGroups: - - policy - resources: - - poddisruptionbudgets/status - verbs: - - get - - update -- apiGroups: - - traefik.io - resources: - - ingressroutes - - middlewares - verbs: - - create - - delete - - get - - list - - update - - watch diff --git a/config/samples/samples.go b/config/samples/samples.go deleted file mode 100644 index 48ae9f3..0000000 --- a/config/samples/samples.go +++ /dev/null @@ -1,17 +0,0 @@ -package samples - -import ( - _ "embed" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "sigs.k8s.io/yaml" -) - -//go:embed v3_wfs.yaml -var v3WFSContent string - -func V3WFS() (*pdoknlv3.WFS, error) { - var sample pdoknlv3.WFS - err := yaml.Unmarshal([]byte(v3WFSContent), &sample) - return &sample, err -} diff --git a/config/samples/v2beta1_wfs.yaml b/config/samples/v2beta1_wfs.yaml index 2e3191d..db2924f 100644 --- a/config/samples/v2beta1_wfs.yaml +++ b/config/samples/v2beta1_wfs.yaml @@ -1,63 +1,9 @@ apiVersion: pdok.nl/v2beta1 kind: WFS metadata: - name: sample-v2 labels: app.kubernetes.io/name: mapserver-operator app.kubernetes.io/managed-by: kustomize - dataset: dataset - dataset-owner: eigenaar - service-version: v1_0 - service-type: wfs - annotations: - lifecycle-phase: prod - service-bundle-id: e9f89184-d8c3-5600-8502-08e8e9bc9d2f + name: wfs-sample spec: - general: - datasetOwner: eigenaar - serviceVersion: v1_0 - dataset: dataset - kubernetes: - resources: - limits: - ephemeralStorage: 20Mi - options: - automaticCasing: true - includeIngress: true - service: - inspire: true - title: Dataset - abstract: "Dataset beschrijving..." - keywords: - - keyword1 - - keyword2 - accessConstraints: none - metadataIdentifier: 68a42961-ed55-436b-a412-cc7424fd2a6e - authority: - name: eigenaar - url: https://www.rijksoverheid.nl/ministeries/ministerie-van-economische-zaken-en-klimaat - dataEPSG: "EPSG:28992" - extent: "0 300000 280000 625000" - featureTypes: - - name: "feature1" - title: "feature1" - abstract: "Feature 1 beschrijving..." - keywords: - - keyword1 - - keyword2 - datasetMetadataIdentifier: "07d73b60-dfd6-4c54-9c82-9fac70c6c48e" - sourceMetadataIdentifier: "07d73b60-dfd6-4c54-9c82-9fac70c6c48e" # TODO - data: - gpkg: - blobKey: eigenaar/dataset/data.gpkg - table: "table1" - geometryType: "MultiPolygon" - columns: - - "naam" - - "gebiedsnum" - - "besluitnum" - - "besluitdat" - aliases: - gebiedsnum: gebiedsnummer - besluitdat: datum - + # TODO(user): Add fields here diff --git a/config/samples/v3_wfs.yaml b/config/samples/v3_wfs.yaml index 06bb5cb..4b3691b 100644 --- a/config/samples/v3_wfs.yaml +++ b/config/samples/v3_wfs.yaml @@ -1,42 +1,9 @@ apiVersion: pdok.nl/v3 kind: WFS metadata: - name: sample labels: - pdok.nl/owner-id: pdok - pdok.nl/dataset-id: sample + app.kubernetes.io/name: mapserver-operator + app.kubernetes.io/managed-by: kustomize + name: wfs-sample spec: - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 2G - service: - inspire: - language: dut - serviceMetadataUrl: - csw: - metadataIdentifier: 655549bd-8c05-4c69-950b-ad1e346dcac9 - spatialDatasetIdentifier: 90af202c-de3a-4fbf-901c-82ae703904e3 - title: "title" - abstract: "abstract" - defaultCrs: "EPSG:28992" - keywords: - - "keyword" - ownerInfoRef: "owner" - prefix: "prefix" - url: "http://host/path" - featureTypes: - - name: "name" - title: "title" - abstract: "abstract" - keywords: - - "word" - data: - gpkg: - blobKey: "container/prefix/file.gpkg" - columns: - - name: "column" - geometryType: "Point" - tableName: "table" + # TODO(user): Add fields here diff --git a/config/samples/v3_wms.yaml b/config/samples/v3_wms.yaml index b4aae82..ed961c2 100644 --- a/config/samples/v3_wms.yaml +++ b/config/samples/v3_wms.yaml @@ -1,70 +1,9 @@ apiVersion: pdok.nl/v3 kind: WMS metadata: - name: sample labels: - pdok.nl/owner-id: pdok - pdok.nl/dataset-id: sample + app.kubernetes.io/name: mapserver-operator + app.kubernetes.io/managed-by: kustomize + name: wms-sample spec: - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 1m - service: - prefix: "prefix" - url: "https://test.test/path" - title: "title" - abstract: "abstract" - keywords: - - "keyword" - ownerInfoRef: pdok - dataEPSG: "EPSG:28992" - stylingAssets: - configMapRefs: - - name: configmap - keys: - - file.symbol - layer: - title: "title" - abstract: "abstract" - keywords: - - "keyword" - visible: true - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - layers: - - name: "visible" - visible: true - title: "title" - abstract: "abstract" - keywords: - - keyword - data: - gpkg: - blobKey: "container/path/file.gpkg" - columns: - - name: "column" - geometryType: "Point" - tableName: "table" - styles: - - name: "name" - title: "title" - visualization: file.symbol - - name: "not visible" - visible: false - data: - postgis: - columns: - - name: "column" - geometryType: "Point" - tableName: "table" - styles: - - name: "name" - visualization: file.symbol + # TODO(user): Add fields here diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 490c033..459a32b 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -24,23 +24,3 @@ webhooks: resources: - wfs sideEffects: None -- admissionReviewVersions: - - v1 - clientConfig: - service: - name: webhook-service - namespace: system - path: /validate-pdok-nl-v3-wms - failurePolicy: Fail - name: vwms-v3.kb.io - rules: - - apiGroups: - - pdok.nl - apiVersions: - - v3 - operations: - - CREATE - - UPDATE - resources: - - wms - sideEffects: None diff --git a/go.mod b/go.mod index 192a905..665a78d 100644 --- a/go.mod +++ b/go.mod @@ -1,148 +1,100 @@ module github.com/pdok/mapserver-operator -go 1.24.0 - -toolchain go1.24.2 +go 1.23.0 godebug default=go1.23 require ( - github.com/cbroglie/mustache v1.4.0 - github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.37.0 - github.com/pdok/featureinfo-generator v1.4.0 - github.com/pdok/ogc-capabilities-generator v1.0.1 - github.com/pdok/ogc-specifications v1.0.0 - github.com/pdok/smooth-operator v1.2.2 - github.com/peterbourgon/ff v1.7.1 - github.com/stretchr/testify v1.10.0 - github.com/traefik/traefik/v3 v3.4.1 - k8s.io/api v0.33.1 - k8s.io/apimachinery v0.33.1 - k8s.io/client-go v0.33.1 - sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/yaml v1.4.0 -) - -replace github.com/abbot/go-http-auth => github.com/abbot/go-http-auth v0.4.0 // for github.com/traefik/traefik/v3 - -require ( - github.com/aws/smithy-go v1.22.2 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect - github.com/go-acme/lego/v4 v4.23.1 // indirect - github.com/go-jose/go-jose/v4 v4.0.5 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/google/go-github/v28 v28.1.1 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect - github.com/http-wasm/http-wasm-host-go v0.7.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/miekg/dns v1.1.64 // indirect - github.com/patrickmn/go-cache v2.1.0+incompatible // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/rs/zerolog v1.33.0 // indirect - github.com/traefik/paerser v0.2.2 // indirect - github.com/unrolled/render v1.0.2 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect - go.etcd.io/etcd/client/v3 v3.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect - go.opentelemetry.io/otel/log v0.8.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/mod v0.24.0 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect + github.com/onsi/ginkgo/v2 v2.22.0 + github.com/onsi/gomega v1.36.1 + k8s.io/apimachinery v0.32.1 + k8s.io/client-go v0.32.1 + sigs.k8s.io/controller-runtime v0.20.2 ) require ( - cel.dev/expr v0.24.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + cel.dev/expr v0.18.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect; indirectC - github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/go-errors/errors v1.5.1 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-logr/zapr v1.3.0 - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.25.0 // indirect - github.com/google/gnostic-models v0.6.9 // indirect - github.com/google/go-cmp v0.7.0 - github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect + github.com/google/cel-go v0.22.0 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.64.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/spf13/cobra v1.9.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/cobra v1.8.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect - golang.org/x/net v0.40.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.33.0 - gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/grpc v1.72.2 // indirect - google.golang.org/protobuf v1.36.6 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.26.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect + google.golang.org/grpc v1.65.0 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 - k8s.io/apiextensions-apiserver v0.33.1 - k8s.io/apiserver v0.33.1 // indirect - k8s.io/component-base v0.33.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.32.1 // indirect + k8s.io/apiextensions-apiserver v0.32.1 // indirect + k8s.io/apiserver v0.32.1 // indirect + k8s.io/component-base v0.32.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect - sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect - sigs.k8s.io/kustomize/api v0.19.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index ac4bf9c..b257e61 100644 --- a/go.sum +++ b/go.sum @@ -1,110 +1,74 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= -github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= +cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= -github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= -github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= -github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV4= -github.com/go-acme/lego/v4 v4.23.1/go.mod h1:7UMVR7oQbIYw6V7mTgGwi4Er7B6Ww0c+c8feiBM0EgI= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= -github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= +github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= -github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4= -github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/http-wasm/http-wasm-host-go v0.7.0 h1:+1KrRyOO6tWiDB24QrtSYyDmzFLBBs3jioKaUT0mq1c= -github.com/http-wasm/http-wasm-host-go v0.7.0/go.mod h1:adXKcLmL7yuavH/e0kBAp7b3TgAHTo/enCduyN5bXGM= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -113,26 +77,15 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= -github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -140,113 +93,61 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= -github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pdok/featureinfo-generator v1.4.0 h1:AqtLxdLj3nnBxjsnPCRKyddOyhq52h40OEjAbNJgIYE= -github.com/pdok/featureinfo-generator v1.4.0/go.mod h1:02Ryu7ZRkeha8SCfS6VYWCdKYZh0llskrfFgp6xRCjk= -github.com/pdok/ogc-capabilities-generator v1.0.1 h1:7cKgdcWO4y+M9a2hW8ONhmBxGERdva/WB4JBaB9+91s= -github.com/pdok/ogc-capabilities-generator v1.0.1/go.mod h1:Fnq2i1X3Aufjx/ptQ34c9knw1mDMUAQt0MTBscqX0/M= -github.com/pdok/ogc-specifications v1.0.0 h1:YqVqKwgOrPprTuRcNrFJeUuxSHOIeOSQStBcYJZUGcA= -github.com/pdok/ogc-specifications v1.0.0/go.mod h1:YDngwkwrWOfc5MYnEYseiv97K1Y9bZXlVzwi/8EaIl8= -github.com/pdok/smooth-operator v1.2.2 h1:g6wq77mbK335KMb73Hn/7LnRA8VVqtdr7hj+tjrdoiM= -github.com/pdok/smooth-operator v1.2.2/go.mod h1:tqr/CDCXZHNzQzQVlSAnCmsPlx9tWAObsj8hg9mSSEU= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= -github.com/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk= -github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ= -github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24= -github.com/traefik/traefik/v3 v3.4.1 h1:QBO/C9ILViPVBhsmY8nEnoobTULxg6oW1jUTX8FFh8w= -github.com/traefik/traefik/v3 v3.4.1/go.mod h1:8FHoFbX5P+zMQ3UUjjfrDH87BDSbHllcUQyiI2wCP3o= -github.com/unrolled/render v1.0.2 h1:dGS3EmChQP3yOi1YeFNO/Dx+MbWZhdvhQJTXochM5bs= -github.com/unrolled/render v1.0.2/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/etcd/api/v3 v3.6.0 h1:vdbkcUBGLf1vfopoGE/uS3Nv0KPyIpUV/HM6w9yx2kM= -go.etcd.io/etcd/api/v3 v3.6.0/go.mod h1:Wt5yZqEmxgTNJGHob7mTVBJDZNXiHPtXTcPab37iFOw= -go.etcd.io/etcd/client/pkg/v3 v3.6.0 h1:nchnPqpuxvv3UuGGHaz0DQKYi5EIW5wOYsgUNRc365k= -go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA= -go.etcd.io/etcd/client/v3 v3.6.0 h1:/yjKzD+HW5v/3DVj9tpwFxzNbu8hjcKID183ug9duWk= -go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8DwPdMJMg= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/collector/pdata v1.10.0 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo01pteS4WOE= -go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= -go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -256,68 +157,56 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= -gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= -google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= +google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -325,42 +214,34 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= +k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= +k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= +k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= +k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= +k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= +k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= +k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= +k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= +k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= +k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= -k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= -sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= -sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= -sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= -sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc= +sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 02c63b7..221dcbe 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,23 +1,15 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ \ No newline at end of file diff --git a/internal/controller/blobdownload/blob_download.go b/internal/controller/blobdownload/blob_download.go deleted file mode 100644 index 7c439f7..0000000 --- a/internal/controller/blobdownload/blob_download.go +++ /dev/null @@ -1,225 +0,0 @@ -package blobdownload - -import ( - _ "embed" - "fmt" - "regexp" - "strings" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - "github.com/pdok/mapserver-operator/internal/controller/types" - - "k8s.io/utils/strings/slices" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/mapperutils" - "github.com/pdok/mapserver-operator/internal/controller/utils" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -const ( - tifPath = "/srv/data/tif" - imagesPath = "/srv/data/images" - fontsPath = "/srv/data/config/fonts" - legendPath = "/var/www/legend" -) - -//go:embed gpkg_download.sh -var GpkgDownloadScript string - -func GetScript() string { - return GpkgDownloadScript -} - -func GetBlobDownloadInitContainer[O pdoknlv3.WMSWFS](obj O, images types.Images) (*corev1.Container, error) { - blobkeys := []string{} - for _, gpkg := range obj.GeoPackages() { - // Deduplicate blobkeys to prevent double downloads - if !slices.Contains(blobkeys, gpkg.BlobKey) { - blobkeys = append(blobkeys, gpkg.BlobKey) - } - } - - initContainer := corev1.Container{ - Name: constants.BlobDownloadName, - Image: images.MultitoolImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Env: []corev1.EnvVar{ - { - Name: "GEOPACKAGE_TARGET_PATH", - Value: "/srv/data/gpkg", - }, - { - Name: "GEOPACKAGE_DOWNLOAD_LIST", - Value: strings.Join(blobkeys, ";"), - }, - }, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("0.15"), - }, - }, - Command: []string{"/bin/sh", "-c"}, - VolumeMounts: []corev1.VolumeMount{ - utils.GetBaseVolumeMount(), - utils.GetDataVolumeMount(), - }, - } - - // Additional blob-download configuration - args, err := GetArgs(obj) - if err != nil { - return nil, err - } - initContainer.Args = []string{args} - - resourceCPU := resource.MustParse("0.2") - if use, _ := mapperutils.UseEphemeralVolume(obj); use { - resourceCPU = resource.MustParse("1") - } - initContainer.Resources.Limits = corev1.ResourceList{ - corev1.ResourceCPU: resourceCPU, - } - - if obj.Options().PrefetchData { - mount := corev1.VolumeMount{Name: constants.InitScriptsName, MountPath: "/srv/scripts", ReadOnly: true} - initContainer.VolumeMounts = append(initContainer.VolumeMounts, mount) - } - - return &initContainer, nil -} - -func GetArgs[W pdoknlv3.WMSWFS](webservice W) (args string, err error) { - var sb strings.Builder - - switch any(webservice).(type) { - case *pdoknlv3.WFS: - if WFS, ok := any(webservice).(*pdoknlv3.WFS); ok { - createConfig(&sb) - downloadGeopackage(&sb, WFS.Options().PrefetchData) - // In case of WFS no downloads are needed for TIFFs, styling assets and legends - } - case *pdoknlv3.WMS: - if WMS, ok := any(webservice).(*pdoknlv3.WMS); ok { - createConfig(&sb) - downloadGeopackage(&sb, WMS.Options().PrefetchData) - if err = downloadTiffs(&sb, WMS); err != nil { - return "", err - } - if err = downloadStylingAssets(&sb, WMS); err != nil { - return "", err - } - if err = downloadLegends(&sb, WMS); err != nil { - return "", err - } - } - default: - return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) - } - return sb.String(), nil -} - -func createConfig(sb *strings.Builder) { - writeLine(sb, "set -e;") - writeLine(sb, "mkdir -p /srv/data/config/;") - writeLine(sb, "rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true;") -} - -func downloadGeopackage(sb *strings.Builder, prefetchData bool) { - if prefetchData { - writeLine(sb, "bash /srv/scripts/gpkg_download.sh;") - } -} - -func downloadTiffs(sb *strings.Builder, wms *pdoknlv3.WMS) error { - if !wms.Options().PrefetchData { - return nil - } - - for _, blobKey := range wms.GetUniqueTiffBlobKeys() { - fileName, err := getFilenameFromBlobKey(blobKey) - if err != nil { - return err - } - writeLine(sb, "rclone copyto blobs:/%s %s/%s || exit 1;", blobKey, tifPath, fileName) - } - return nil -} - -func downloadStylingAssets(sb *strings.Builder, wms *pdoknlv3.WMS) error { - if wms.Spec.Service.StylingAssets == nil { // TODO Is StylingAssets required and should this return an error? - return nil - } - - generatedFontsList := false - re := regexp.MustCompile(`.*\.(ttf)$`) - for _, blobKey := range wms.Spec.Service.StylingAssets.BlobKeys { - fileName, err := getFilenameFromBlobKey(blobKey) - if err != nil { - return err - } - path := imagesPath - isTTF := re.MatchString(fileName) - if isTTF { - path = fontsPath - } - writeLine(sb, "rclone copyto blobs:/%s %s/%s || exit 1;", blobKey, path, fileName) - if isTTF { - fileRoot, err := getRootFromFilename(fileName) - if err != nil { - return err - } - writeLine(sb, "echo %s %s >> %s/fonts.list;", fileRoot, fileName, fontsPath) - generatedFontsList = true - } - } - - if generatedFontsList { - writeLine(sb, "echo 'generated fonts.list:';") - writeLine(sb, "cat %v/fonts.list;", fontsPath) - } - - return nil -} - -func downloadLegends(sb *strings.Builder, wms *pdoknlv3.WMS) error { - layers := wms.GetAllLayersWithLegend() - if len(layers) > 0 { - for _, layer := range layers { - writeLine(sb, "mkdir -p %s/%s;", legendPath, *layer.Name) - for _, style := range layer.Styles { - writeLine(sb, "rclone copyto blobs:/%s %s/%s/%s.png || exit 1;", style.Legend.BlobKey, legendPath, *layer.Name, style.Name) - fileName, err := getFilenameFromBlobKey(style.Legend.BlobKey) - if err != nil { - return err - } - writeLine(sb, "echo 'Copied legend %s to %s/%s/%s.png';", fileName, legendPath, *layer.Name, style.Name) - } - } - writeLine(sb, "chown -R 999:999 %s", legendPath) - } - - return nil -} - -func getFilenameFromBlobKey(blobKey string) (string, error) { - index := strings.LastIndex(blobKey, "/") - if index == -1 { - return "", fmt.Errorf("could not determine filename from blobkey %s", blobKey) - } - return blobKey[index+1:], nil -} - -func getRootFromFilename(fileName string) (string, error) { - index := strings.LastIndex(fileName, ".") - if index == -1 { - return "", fmt.Errorf("could not determine root from filename %s", fileName) - } - return fileName[:index], nil -} - -func writeLine(sb *strings.Builder, format string, a ...any) { - sb.WriteString(fmt.Sprintf(format, a...) + "\n") -} diff --git a/internal/controller/blobdownload/blob_download_test.go b/internal/controller/blobdownload/blob_download_test.go deleted file mode 100644 index 81b87b4..0000000 --- a/internal/controller/blobdownload/blob_download_test.go +++ /dev/null @@ -1,332 +0,0 @@ -package blobdownload - -import ( - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -const ( - WFSArgsWithPrefetch = `set -e; -mkdir -p /srv/data/config/; -rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; -bash /srv/scripts/gpkg_download.sh; -` - WFSArgsWithoutPrefetch = `set -e; -mkdir -p /srv/data/config/; -rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; -` - - WMSArgsForGeoPackageLayers = `set -e; -mkdir -p /srv/data/config/; -rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; -bash /srv/scripts/gpkg_download.sh; -rclone copyto blobs:/resources-bucket/key/gpkg-symbol.png /srv/data/images/gpkg-symbol.png || exit 1; -rclone copyto blobs:/resources-bucket/key/symbol.svg /srv/data/images/symbol.svg || exit 1; -rclone copyto blobs:/resources-bucket/key/font-1.ttf /srv/data/config/fonts/font-1.ttf || exit 1; -echo font-1 font-1.ttf >> /srv/data/config/fonts/fonts.list; -rclone copyto blobs:/resources-bucket/key/font-2.ttf /srv/data/config/fonts/font-2.ttf || exit 1; -echo font-2 font-2.ttf >> /srv/data/config/fonts/fonts.list; -echo 'generated fonts.list:'; -cat /srv/data/config/fonts/fonts.list; -mkdir -p /var/www/legend/wms-gpkg-layer-1-name; -rclone copyto blobs:/resources-bucket/key/gpkg-layer-1-legend.png /var/www/legend/wms-gpkg-layer-1-name/wms-gpkg-style-1-name.png || exit 1; -echo 'Copied legend gpkg-layer-1-legend.png to /var/www/legend/wms-gpkg-layer-1-name/wms-gpkg-style-1-name.png'; -mkdir -p /var/www/legend/wms-gpkg-layer-2-name; -rclone copyto blobs:/resources-bucket/key/gpkg-layer-2-legend.png /var/www/legend/wms-gpkg-layer-2-name/wms-gpkg-style-2-name.png || exit 1; -echo 'Copied legend gpkg-layer-2-legend.png to /var/www/legend/wms-gpkg-layer-2-name/wms-gpkg-style-2-name.png'; -chown -R 999:999 /var/www/legend -` - - WMSArgsForTIFLayers = `set -e; -mkdir -p /srv/data/config/; -rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; -bash /srv/scripts/gpkg_download.sh; -rclone copyto blobs:/tifs-bucket/key/tif-layer-1-data.tif /srv/data/tif/tif-layer-1-data.tif || exit 1; -rclone copyto blobs:/tifs-bucket/key/tif-layer-2-data.tif /srv/data/tif/tif-layer-2-data.tif || exit 1; -rclone copyto blobs:/resources-bucket/key/tif-symbol.png /srv/data/images/tif-symbol.png || exit 1; -rclone copyto blobs:/resources-bucket/key/symbol.svg /srv/data/images/symbol.svg || exit 1; -rclone copyto blobs:/resources-bucket/key/font-1.ttf /srv/data/config/fonts/font-1.ttf || exit 1; -echo font-1 font-1.ttf >> /srv/data/config/fonts/fonts.list; -rclone copyto blobs:/resources-bucket/key/font-2.ttf /srv/data/config/fonts/font-2.ttf || exit 1; -echo font-2 font-2.ttf >> /srv/data/config/fonts/fonts.list; -echo 'generated fonts.list:'; -cat /srv/data/config/fonts/fonts.list; -mkdir -p /var/www/legend/wms-tif-layer-1-name; -rclone copyto blobs:/resources-bucket/key/tif-layer-1-legend.png /var/www/legend/wms-tif-layer-1-name/wms-tif-style-1-name.png || exit 1; -echo 'Copied legend tif-layer-1-legend.png to /var/www/legend/wms-tif-layer-1-name/wms-tif-style-1-name.png'; -mkdir -p /var/www/legend/wms-tif-layer-2-name; -rclone copyto blobs:/resources-bucket/key/tif-layer-2-legend.png /var/www/legend/wms-tif-layer-2-name/wms-tif-style-2-name.png || exit 1; -echo 'Copied legend tif-layer-2-legend.png to /var/www/legend/wms-tif-layer-2-name/wms-tif-style-2-name.png'; -chown -R 999:999 /var/www/legend -` -) - -func TestGetArgsForWFS(t *testing.T) { - type args struct { - WFS *pdoknlv3.WFS - } - tests := []struct { - name string - args args - wantArgs string - wantErr bool - }{ - { - name: "GetArgs for WFS with prefetchData", - args: args{ - WFS: &pdoknlv3.WFS{ - Spec: pdoknlv3.WFSSpec{ - Service: pdoknlv3.WFSService{BaseService: pdoknlv3.BaseService{ - Title: "wfs-prefetch-service-title", - }}, - Options: &pdoknlv3.BaseOptions{ - PrefetchData: true, - }, - }, - }, - }, - wantArgs: WFSArgsWithPrefetch, - wantErr: false, - }, - { - name: "GetArgs for WFS without prefetchData", - args: args{ - WFS: &pdoknlv3.WFS{ - Spec: pdoknlv3.WFSSpec{ - Service: pdoknlv3.WFSService{BaseService: pdoknlv3.BaseService{ - Title: "wfs-noprefetch-service-title", - }}, - Options: &pdoknlv3.BaseOptions{ - PrefetchData: false, - }, - }, - }, - }, - wantArgs: WFSArgsWithoutPrefetch, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - args, err := GetArgs(tt.args.WFS) - if (err != nil) != tt.wantErr { - t.Errorf("GetArgs() error = %v, wantErr %v", err, tt.wantErr) - return - } - if args == "" { - t.Errorf("The returned arguments are empty.") - } - if args != tt.wantArgs { - t.Errorf("GetArgs() = %v, want %v", args, tt.wantArgs) - return - } - }) - } -} - -func TestGetArgsForWMS(t *testing.T) { - type args struct { - WMS pdoknlv3.WMS - } - tests := []struct { - name string - args args - wantArgs string - wantErr bool - }{ - { - name: "GetArgs for WMS GeoPackage layer", - args: args{ - WMS: pdoknlv3.WMS{ - Spec: pdoknlv3.WMSSpec{ - Service: pdoknlv3.WMSService{BaseService: pdoknlv3.BaseService{ - Title: "wms-gpkg-service-title"}, - Layer: pdoknlv3.Layer{ - Name: smoothoperatorutils.Pointer("wms-gpkg-layer-name"), - Title: smoothoperatorutils.Pointer("wms-gpkg-layer-title"), - Styles: []pdoknlv3.Style{ - { - Legend: &pdoknlv3.Legend{ - BlobKey: "key/gpkg-layer-legend.png", - }, - }, - }, - Layers: []pdoknlv3.Layer{ - { - Name: smoothoperatorutils.Pointer("wms-gpkg-layer-1-name"), - Title: smoothoperatorutils.Pointer("wms-gpkg-layer-1-title"), - Styles: []pdoknlv3.Style{ - { - Name: "wms-gpkg-style-1-name", - Title: smoothoperatorutils.Pointer("wms-gpkg-style-1-title"), - Legend: &pdoknlv3.Legend{ - Width: 50, - Height: 50, - Format: "png", - BlobKey: "resources-bucket/key/gpkg-layer-1-legend.png", - }, - }, - }, - Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ - Gpkg: &pdoknlv3.Gpkg{ - BlobKey: "geopackages-bucket/key/gpkg-layer-1-data.gpkg", - }}, - }, - }, - { - Name: smoothoperatorutils.Pointer("wms-gpkg-layer-2-name"), - Title: smoothoperatorutils.Pointer("wms-gpkg-layer-2-title"), - Styles: []pdoknlv3.Style{ - { - Name: "wms-gpkg-style-2-name", - Title: smoothoperatorutils.Pointer("wms-gpkg-style-2-title"), - Legend: &pdoknlv3.Legend{ - BlobKey: "resources-bucket/key/gpkg-layer-2-legend.png", - }, - }, - }, - Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ - Gpkg: &pdoknlv3.Gpkg{ - BlobKey: "geopackages-bucket/key/gpkg-layer-2-data.gpkg", - }}, - }, - }, - }, - }, - StylingAssets: &pdoknlv3.StylingAssets{ - BlobKeys: []string{ - "resources-bucket/key/gpkg-symbol.png", - "resources-bucket/key/symbol.svg", - "resources-bucket/key/font-1.ttf", - "resources-bucket/key/font-2.ttf", - }, - }, - }, - Options: &pdoknlv3.Options{BaseOptions: pdoknlv3.BaseOptions{ - PrefetchData: true, - }}, - }, - }, - }, - wantArgs: WMSArgsForGeoPackageLayers, - wantErr: false, - }, - { - name: "GetArgs for WMS TIF layer", - args: args{ - WMS: pdoknlv3.WMS{ - Spec: pdoknlv3.WMSSpec{ - Service: pdoknlv3.WMSService{BaseService: pdoknlv3.BaseService{ - Title: "wms-tif-service-title"}, - Layer: pdoknlv3.Layer{ - Name: smoothoperatorutils.Pointer("wms-tif-layer-name"), - Title: smoothoperatorutils.Pointer("wms-tif-layer-title"), - Layers: []pdoknlv3.Layer{ - { - Name: smoothoperatorutils.Pointer("wms-tif-layer-1-name"), - Title: smoothoperatorutils.Pointer("wms-tif-layer-1-title"), - Styles: []pdoknlv3.Style{ - { - Name: "wms-tif-style-1-name", - Title: smoothoperatorutils.Pointer("wms-tif-style-1-title"), - Legend: &pdoknlv3.Legend{ - BlobKey: "resources-bucket/key/tif-layer-1-legend.png", - }, - }, - }, - Data: &pdoknlv3.Data{ - TIF: &pdoknlv3.TIF{ - BlobKey: "tifs-bucket/key/tif-layer-1-data.tif", - }, - }, - }, - { - Name: smoothoperatorutils.Pointer("wms-tif-layer-2-name"), - Title: smoothoperatorutils.Pointer("wms-tif-layer-2-title"), - Styles: []pdoknlv3.Style{ - { - Name: "wms-tif-style-2-name", - Title: smoothoperatorutils.Pointer("wms-tif-style-2-title"), - Legend: &pdoknlv3.Legend{ - BlobKey: "resources-bucket/key/tif-layer-2-legend.png", - }, - }, - }, - Data: &pdoknlv3.Data{ - TIF: &pdoknlv3.TIF{ - BlobKey: "tifs-bucket/key/tif-layer-2-data.tif", - }, - }, - }, - }, - }, - StylingAssets: &pdoknlv3.StylingAssets{ - BlobKeys: []string{ - "resources-bucket/key/tif-symbol.png", - "resources-bucket/key/symbol.svg", - "resources-bucket/key/font-1.ttf", - "resources-bucket/key/font-2.ttf", - }, - }, - }, - Options: &pdoknlv3.Options{BaseOptions: pdoknlv3.BaseOptions{ - PrefetchData: true, - }}, - }, - }, - }, - wantArgs: WMSArgsForTIFLayers, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - args, err := GetArgs(&tt.args.WMS) - if (err != nil) != tt.wantErr { - t.Errorf("GetArgs() error = %v, wantErr %v", err, tt.wantErr) - return - } - if args != tt.wantArgs { - diff := cmp.Diff(tt.wantArgs, args) - t.Errorf("GetArgs() -want, +got %s", diff) - return - } - }) - } -} - -func TestGetScript(t *testing.T) { - tests := []struct { - name string - wantHeader string - wantFunctions []string - wantErr bool - }{ - { - name: "Test for expected header and functions in the returned bash script", - wantHeader: "#!/usr/bin/env bash", - wantFunctions: []string{"download_gpkg", "download", "download_all", "rm_file_and_exit"}, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - script := GetScript() - if !strings.HasPrefix(script, tt.wantHeader) { - t.Errorf("The returned script doesn't contain the expected header `%v`, got = %v", tt.wantHeader, script) - } - - for _, function := range tt.wantFunctions { - funcString := "function " + function + "()" - if !strings.Contains(script, funcString) { - t.Errorf("The returned script doesn't contain the expected function `%v`, got = %v", funcString, script) - } - } - }) - } -} diff --git a/internal/controller/blobdownload/gpkg_download.sh b/internal/controller/blobdownload/gpkg_download.sh deleted file mode 100644 index 53a43a7..0000000 --- a/internal/controller/blobdownload/gpkg_download.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -function download_gpkg() { - local gpkg=$1 - local file=$2 - local url=$3 - - if [ -f "$file" ] && [ ! -f "$file".st ]; then - echo msg=\"File already downloaded\" file=\""$file"\" - else - echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" - - # use curl to check if resource exists - # axel blocks on non-existing resources - curl -IfsS "$url" > /dev/null - - echo start "$gpkg" - ret=0 - # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. - axel -n 1 -T 120 -o "$file" "$url" \ - | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ - | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? - - if [ $ret -ne 0 ] - then - echo -e '\n' - # Download failed ($? != 0). - if [ $ret -eq 1 ] - then - # Axel was not able to resume ($? == 1). Remove file and state file. - if [ -f "$file" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file"\" - rm "$file" - fi - if [ -f "$file.st" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file".st\" - rm "$file".st - fi - else - # Download failed with other error ($? > 1). Remove file if state file does not exist. - if [ ! -f "$file.st" ]; then - echo msg=\"Download failed without state file, removing file\" file=\""$file"\" - rm "$file" - fi - fi - - # Retry the download - echo msg=\"Retry file\" file=\""$file"\" - download_gpkg $gpkg $file $url - fi - fi -} - -function download() { - if [ -z "$BLOBS_ENDPOINT" ]; - then - echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; - exit 1; - fi - - local gpkg=$1 - local file=$GEOPACKAGE_TARGET_PATH/$2 - local url=${BLOBS_ENDPOINT}/${gpkg} - - download_gpkg $gpkg $file $url - - # Check Content-length - download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') - file_size=$(wc -c "$file" | awk '{print $1}') - if [ "$download_size" != "$file_size" ] - then - echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - rm_file_and_exit - else - echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - chown 999:999 "$file" - fi - - # Check ogrinfo - echo "Check gpkg with ogrinfo" - if ! ogrinfo -so "$file" - then - echo "ERROR: ogrinfo check on $file failed" - rm_file_and_exit - fi - - # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) - echo "Check if md5 hash value exists in blob storage" - rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" - - # If file contains valid hash, then check it, else skip - hash=$(awk '{ print $1 }' "${file}.md5sum-remote") - if [[ $hash =~ ^[a-f0-9]{32}$ ]] - then - echo "Valid hash value found" - echo "Compare MD5 hash of remote and downloaded gpkg" - if ! (echo "$hash $file" | md5sum --check); then - rm_file_and_exit - fi - else - echo "No hash found for $file in blob storage, skipping checksum." - fi - - # After successful download set the GPKG to readonly - chmod -wx $file - echo "done" -} - -function download_all() { - echo msg=\"Starting GeoPackage downloader\" - - local start_time=$(date '+%s') - - # create target location if not exists - mkdir -p $GEOPACKAGE_TARGET_PATH - chown 999:999 $GEOPACKAGE_TARGET_PATH - - # Download all geopackages from GEOPACKAGE_DOWNLOAD_LIST - # Example: GEOPACKAGE_DOWNLOAD_LIST=path/1/file.gpkg;path/3/other_file.gpkg - gpkgs=(${GEOPACKAGE_DOWNLOAD_LIST//;/ }) - for gpkg_path in "${gpkgs[@]}" - do - filename=$(basename $gpkg_path) - download $gpkg_path $filename - done - - echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) -} - -function rm_file_and_exit() { - echo "Removing $file, to ensure a fresh new download is started when script is executed again" - rm -rf "$file" - - if [ -f "$file.st" ]; then - rm "$file".st - fi - - echo "Exiting..." - exit 1 -} - -download_all | awk -W interactive ' -BEGIN { - state="idle"; -} - -{ - if ($0 != "") { - if ($1 == "start") { - gpkg=$2; - state="downloading"; - } else if ($1 == "done") { - state="idle"; - } else if (state == "downloading") { - if ($1 == "progress") { - # reduce output to prevent loki from choking on large log volume - if (last_percentage != $2) { - if ($3 == "") { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; - } else { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; - } - } - last_percentage=$2; - } else { - print "msg=\"" $0 "\" gpkg=" gpkg; - } - } else { - print $0; - } - } -} -' diff --git a/internal/controller/capabilitiesgenerator/capabilities_generator.go b/internal/controller/capabilitiesgenerator/capabilities_generator.go deleted file mode 100644 index 68fd51f..0000000 --- a/internal/controller/capabilitiesgenerator/capabilities_generator.go +++ /dev/null @@ -1,76 +0,0 @@ -package capabilitiesgenerator - -import ( - "fmt" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - "github.com/pdok/mapserver-operator/internal/controller/types" - "github.com/pdok/mapserver-operator/internal/controller/utils" - "gopkg.in/yaml.v3" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - corev1 "k8s.io/api/core/v1" -) - -func GetCapabilitiesGeneratorInitContainer[O pdoknlv3.WMSWFS](_ O, images types.Images) (*corev1.Container, error) { - initContainer := corev1.Container{ - Name: constants.CapabilitiesGeneratorName, - Image: images.CapabilitiesGeneratorImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Env: []corev1.EnvVar{ - { - Name: "SERVICECONFIG", - Value: "/input/input.yaml", - }, - }, - VolumeMounts: []corev1.VolumeMount{ - utils.GetDataVolumeMount(), - utils.GetConfigVolumeMount(constants.ConfigMapCapabilitiesGeneratorVolumeName), - }, - } - return &initContainer, nil -} - -func GetInput[W pdoknlv3.WMSWFS](webservice W, ownerInfo *smoothoperatorv1.OwnerInfo) (input string, err error) { - switch any(webservice).(type) { - case *pdoknlv3.WFS: - if WFS, ok := any(webservice).(*pdoknlv3.WFS); ok { - return createInputForWFS(WFS, ownerInfo) - } - case *pdoknlv3.WMS: - if WMS, ok := any(webservice).(*pdoknlv3.WMS); ok { - return createInputForWMS(WMS, ownerInfo) - } - default: - return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) - } - return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) -} - -func createInputForWFS(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { - input, err := MapWFSToCapabilitiesGeneratorInput(wfs, ownerInfo) - if err != nil { - return "", err - } - yamlInput, err := yaml.Marshal(input) - if err != nil { - return "", fmt.Errorf("failed to marshal the capabilities generator input to yaml: %w", err) - } - - return string(yamlInput), nil -} - -func createInputForWMS(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { - input, err := MapWMSToCapabilitiesGeneratorInput(wms, ownerInfo) - if err != nil { - return "", err - } - yamlInput, err := yaml.Marshal(input) - if err != nil { - return "", fmt.Errorf("failed to marshal the capabilities generator input to yaml: %w", err) - } - - return string(yamlInput), nil -} diff --git a/internal/controller/capabilitiesgenerator/capabilities_generator_test.go b/internal/controller/capabilitiesgenerator/capabilities_generator_test.go deleted file mode 100644 index cda5991..0000000 --- a/internal/controller/capabilitiesgenerator/capabilities_generator_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package capabilitiesgenerator - -import ( - "github.com/google/go-cmp/cmp" - "github.com/pdok/mapserver-operator/api/v2beta1" - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - capabilitiesgenerator "github.com/pdok/ogc-capabilities-generator/pkg/config" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatormodel "github.com/pdok/smooth-operator/model" - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/yaml" - yamlv3 "sigs.k8s.io/yaml/goyaml.v3" - - "testing" - - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - - _ "embed" -) - -//go:embed test_data/wfs_input.yaml -var WFSInput string - -//go:embed test_data/wms_input.yaml -var WMSInput string - -func TestGetInputForWFS(t *testing.T) { - type args struct { - WFS *pdoknlv3.WFS - ownerInfo *smoothoperatorv1.OwnerInfo - } - url, _ := smoothoperatormodel.ParseURL("http://localhost/datasetOwner/dataset/theme/wfs/v1_0") - pdoknlv3.SetHost("http://localhost") - accessConstraints, _ := smoothoperatormodel.ParseURL("http://creativecommons.org/publicdomain/zero/1.0/deed.nl") - tests := []struct { - name string - args args - wantInput string - wantErr bool - }{ - { - name: "GetInputForWFS", - args: args{ - WFS: &pdoknlv3.WFS{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - "dataset": "dataset", - "dataset-owner": "datasetOwner", - "theme": "theme", - "service-version": "v1_0", - }, - }, - Spec: pdoknlv3.WFSSpec{ - Service: pdoknlv3.WFSService{BaseService: pdoknlv3.BaseService{ - URL: smoothoperatormodel.URL{URL: url}, - Prefix: "prefix", - Title: "some Service title", - Abstract: "some \"Service\" abstract", - Keywords: []string{"service-keyword-1", "service-keyword-2", "infoFeatureAccessService"}, - AccessConstraints: smoothoperatormodel.URL{URL: accessConstraints}}, - Inspire: &pdoknlv3.WFSInspire{Inspire: pdoknlv3.Inspire{ - ServiceMetadataURL: pdoknlv3.MetadataURL{ - CSW: &pdoknlv3.Metadata{ - MetadataIdentifier: "metameta-meta-meta-meta-metametameta", - }, - }, - Language: "dut"}, - SpatialDatasetIdentifier: "datadata-data-data-data-datadatadata", - }, - DefaultCrs: "EPSG:28992", - OtherCrs: []string{ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - }, - FeatureTypes: []pdoknlv3.FeatureType{ - { - Name: "featuretype-1-name", - Title: "featuretype-1-title", - Abstract: "feature \"1\" abstract", - Keywords: []string{"featuretype-1-keyword-1", "featuretype-1-keyword-2"}, - DatasetMetadataURL: &pdoknlv3.MetadataURL{ - CSW: &pdoknlv3.Metadata{ - MetadataIdentifier: "datadata-data-data-data-datadatadata", - }, - }, - Bbox: &pdoknlv3.FeatureBbox{ - WGS84: &smoothoperatormodel.BBox{ - MinX: "-180", - MaxX: "180", - MinY: "-90", - MaxY: "90", - }, - }, - }, - { - Name: "featuretype-2-name", - Title: "featuretype-2-title", - Abstract: "feature \"2\" abstract", - Keywords: []string{"featuretype-2-keyword-1", "featuretype-2-keyword-2"}, - DatasetMetadataURL: &pdoknlv3.MetadataURL{ - CSW: &pdoknlv3.Metadata{ - MetadataIdentifier: "datadata-data-data-data-datadatadata", - }, - }, - }, - }, - }, - }, - }, - ownerInfo: &smoothoperatorv1.OwnerInfo{ - Spec: smoothoperatorv1.OwnerInfoSpec{ - NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), - MetadataUrls: &smoothoperatorv1.MetadataUrls{ - CSW: &smoothoperatorv1.MetadataURL{ - HrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}", - }, - }, - WFS: &smoothoperatorv1.WFS{ - ServiceProvider: smoothoperatorv1.ServiceProvider{ - ProviderName: smoothoperatorutils.Pointer("PDOK"), - }, - }, - }, - }, - }, - wantInput: WFSInput, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotInput, err := GetInput(tt.args.WFS, tt.args.ownerInfo) - if (err != nil) != tt.wantErr { - t.Errorf("GetInput() error = %v, wantErr %v", err, tt.wantErr) - return - } - - wantMap := capabilitiesgenerator.Config{} - gotMap := capabilitiesgenerator.Config{} - err = yamlv3.Unmarshal([]byte(WFSInput), &wantMap) - assert.NoError(t, err) - err = yamlv3.Unmarshal([]byte(gotInput), &gotMap) - assert.NoError(t, err) - - diff := cmp.Diff(wantMap, gotMap) - if diff != "" { - t.Errorf("GetInput() mismatch (-want +got):\n%s", diff) - } - }) - } -} - -func TestInputForWMS(t *testing.T) { - //nolint:misspell - v2wmsstring := "apiVersion: pdok.nl/v2beta1\nkind: WMS\nmetadata:\n name: rws-nwbwegen-v1-0\n labels:\n dataset-owner: rws\n dataset: nwbwegen\n service-version: v1_0\n service-type: wms\n annotations:\n lifecycle-phase: prod\n service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb\nspec:\n general:\n datasetOwner: rws\n dataset: nwbwegen\n serviceVersion: v1_0\n kubernetes:\n healthCheck:\n boundingbox: 135134.89,457152.55,135416.03,457187.82\n resources:\n limits:\n ephemeralStorage: 1535Mi\n memory: 4G\n requests:\n cpu: 2000m\n ephemeralStorage: 1535Mi\n memory: 4G\n options:\n automaticCasing: true\n disableWebserviceProxy: false\n includeIngress: true\n validateRequests: true\n service:\n title: NWB - Wegen WMS\n abstract:\n Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen.\n Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen\n Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland.\n Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk,\n provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien\n van een straatnaam of nummer.\n authority:\n name: rws\n url: https://www.rijkswaterstaat.nl\n dataEPSG: EPSG:28992\n extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961\n inspire: true\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n - Hectometerpunten\n - HVD\n - Mobiliteit\n stylingAssets:\n configMapRefs:\n - name: includes\n keys:\n - nwb_wegen_hectopunten.symbol\n - hectopunten.style\n - wegvakken.style\n blobKeys:\n - resources/fonts/liberation-sans.ttf\n layers:\n - abstract:\n Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB)\n en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer,\n routenummer, wegbeheerder, huisnummers, enz. weer.\n data:\n gpkg:\n columns:\n - objectid\n - wvk_id\n - wvk_begdat\n - jte_id_beg\n - jte_id_end\n - wegbehsrt\n - wegnummer\n - wegdeelltr\n - hecto_lttr\n - bst_code\n - rpe_code\n - admrichtng\n - rijrichtng\n - stt_naam\n - stt_bron\n - wpsnaam\n - gme_id\n - gme_naam\n - hnrstrlnks\n - hnrstrrhts\n - e_hnr_lnks\n - e_hnr_rhts\n - l_hnr_lnks\n - l_hnr_rhts\n - begafstand\n - endafstand\n - beginkm\n - eindkm\n - pos_tv_wol\n - wegbehcode\n - wegbehnaam\n - distrcode\n - distrnaam\n - dienstcode\n - dienstnaam\n - wegtype\n - wgtype_oms\n - routeltr\n - routenr\n - routeltr2\n - routenr2\n - routeltr3\n - routenr3\n - routeltr4\n - routenr4\n - wegnr_aw\n - wegnr_hmp\n - geobron_id\n - geobron_nm\n - bronjaar\n - openlr\n - bag_orl\n - frc\n - fow\n - alt_naam\n - alt_nr\n - rel_hoogte\n - st_lengthshape\n geometryType: MultiLineString\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: wegvakken\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n maxScale: 50000.0\n minScale: 1.0\n name: wegvakken\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: wegvakken\n title: NWB - Wegvakken\n visualization: wegvakken.style\n title: Wegvakken\n visible: true\n - abstract:\n Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB)\n en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand,\n zijde en hectoletter weer.\n data:\n gpkg:\n columns:\n - objectid\n - hectomtrng\n - afstand\n - wvk_id\n - wvk_begdat\n - zijde\n - hecto_lttr\n geometryType: MultiPoint\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: hectopunten\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Hectometerpunten\n maxScale: 50000.0\n minScale: 1.0\n name: hectopunten\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: hectopunten\n title: NWB - Hectopunten\n visualization: hectopunten.style\n title: Hectopunten\n visible: true\n metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8\n" - v2wms := &v2beta1.WMS{} - err := yaml.Unmarshal([]byte(v2wmsstring), v2wms) - assert.NoError(t, err) - pdoknlv3.SetHost("http://localhost") - var wms pdoknlv3.WMS - err = v2wms.ToV3(&wms) - assert.NoError(t, err) - - contactPersonPrimary := smoothoperatorv1.ContactPersonPrimary{ - ContactPerson: smoothoperatorutils.Pointer("KlantContactCenter PDOK"), - ContactOrganization: smoothoperatorutils.Pointer("PDOK"), - } - - ownerInfo := smoothoperatorv1.OwnerInfo{ - Spec: smoothoperatorv1.OwnerInfoSpec{ - NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), - MetadataUrls: &smoothoperatorv1.MetadataUrls{ - CSW: &smoothoperatorv1.MetadataURL{ - HrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}", - }, - }, - ProviderSite: &smoothoperatorv1.ProviderSite{ - Type: "simple", - Href: "https://www.pdok.nl", - }, - WMS: &smoothoperatorv1.WMS{ - ContactInformation: smoothoperatorv1.ContactInformation{ - ContactPersonPrimary: &contactPersonPrimary, - ContactPosition: smoothoperatorutils.Pointer("pointOfContact"), - ContactAddress: &smoothoperatorv1.ContactAddress{ - AddressType: smoothoperatorutils.Pointer("Work"), - Address: nil, - City: smoothoperatorutils.Pointer("Apeldoorn"), - StateOrProvince: nil, - PostCode: nil, - Country: smoothoperatorutils.Pointer("The Netherlands"), - }, - ContactVoiceTelephone: nil, - ContactFacsimileTelephone: nil, - ContactElectronicMailAddress: smoothoperatorutils.Pointer("BeheerPDOK@kadaster.nl"), - }, - }, - }, - } - - input, err := GetInput(&wms, &ownerInfo) - assert.NoError(t, err) - - wantMap := capabilitiesgenerator.Config{} - gotMap := capabilitiesgenerator.Config{} - err = yamlv3.Unmarshal([]byte(WMSInput), &wantMap) - assert.NoError(t, err) - err = yamlv3.Unmarshal([]byte(input), &gotMap) - assert.NoError(t, err) - - diff := cmp.Diff(wantMap, gotMap) - assert.Equal(t, diff, "", "%s", diff) -} diff --git a/internal/controller/capabilitiesgenerator/default_bboxes.go b/internal/controller/capabilitiesgenerator/default_bboxes.go deleted file mode 100644 index 08803ef..0000000 --- a/internal/controller/capabilitiesgenerator/default_bboxes.go +++ /dev/null @@ -1,132 +0,0 @@ -package capabilitiesgenerator - -import "github.com/pdok/ogc-specifications/pkg/wms130" - -// TODO Bounding boxes and default CRSes in this file are not used at the moment but are kept so we can use them again later - -//nolint:unused -var defaultWMSBoundingBox = wms130.EXGeographicBoundingBox{ - WestBoundLongitude: 2.52713, - EastBoundLongitude: 7.37403, - SouthBoundLatitude: 50.2129, - NorthBoundLatitude: 55.7212, -} - -//nolint:unused -func getDefaultWMSCRSes() []wms130.CRS { - return []wms130.CRS{{ - Namespace: "EPSG", - Code: 28992, - }, { - Namespace: "EPSG", - Code: 25831, - }, { - Namespace: "EPSG", - Code: 25832, - }, { - Namespace: "EPSG", - Code: 3034, - }, { - Namespace: "EPSG", - Code: 3035, - }, { - Namespace: "EPSG", - Code: 3857, - }, { - Namespace: "EPSG", - Code: 4258, - }, { - Namespace: "EPSG", - Code: 4326, - }, { - Namespace: "CRS", - Code: 84, - }} -} - -//nolint:unused -func getDefaultWMSLayerBoundingBoxes() []*wms130.LayerBoundingBox { - return []*wms130.LayerBoundingBox{ - { - CRS: "EPSG:28992", - Minx: -25000, - Miny: 250000, - Maxx: 280000, - Maxy: 860000, - Resx: 0, - Resy: 0, - }, - { - CRS: "EPSG:25831", - Minx: -470271, - Miny: 5.56231e+06, - Maxx: 795163, - Maxy: 6.18197e+06, - Resx: 0, - Resy: 0, - }, - { - CRS: "EPSG:25832", - Minx: 62461.6, - Miny: 5.56555e+06, - Maxx: 397827, - Maxy: 6.19042e+06, - Resx: 0, - Resy: 0, - }, - { - CRS: "EPSG:3034", - Minx: 2.61336e+06, - Miny: 3.509e+06, - Maxx: 3.22007e+06, - Maxy: 3.84003e+06, - Resx: 0, - Resy: 0, - }, - { - CRS: "EPSG:3035", - Minx: 3.01676e+06, - Miny: 3.81264e+06, - Maxx: 3.64485e+06, - Maxy: 4.15586e+06, - Resx: 0, - Resy: 0, - }, - { - CRS: "EPSG:3857", - Minx: 281318, - Miny: 6.48322e+06, - Maxx: 820873, - Maxy: 7.50311e+06, - Resx: 0, - Resy: 0, - }, - { - CRS: "EPSG:4258", - Minx: 50.2129, - Miny: 2.52713, - Maxx: 55.7212, - Maxy: 7.37403, - Resx: 0, - Resy: 0, - }, - { - CRS: "EPSG:4326", - Minx: 50.2129, - Miny: 2.52713, - Maxx: 55.7212, - Maxy: 7.37403, - Resx: 0, - Resy: 0, - }, - { - CRS: "CRS:84", - Minx: 2.52713, - Miny: 50.2129, - Maxx: 7.37403, - Maxy: 55.7212, - Resx: 0, - Resy: 0, - }, - } -} diff --git a/internal/controller/capabilitiesgenerator/mapper.go b/internal/controller/capabilitiesgenerator/mapper.go deleted file mode 100644 index 7a3e8d9..0000000 --- a/internal/controller/capabilitiesgenerator/mapper.go +++ /dev/null @@ -1,691 +0,0 @@ -package capabilitiesgenerator - -import ( - "fmt" - "slices" - "strconv" - "strings" - - "k8s.io/utils/ptr" - - "github.com/pdok/ogc-specifications/pkg/wms130" - - "github.com/cbroglie/mustache" - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/mapperutils" - capabilitiesgenerator "github.com/pdok/ogc-capabilities-generator/pkg/config" - "github.com/pdok/ogc-specifications/pkg/wfs200" - "github.com/pdok/ogc-specifications/pkg/wsc110" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -const ( - inspireSchemaLocationsWFS = "http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd" - inspireSchemaLocationsWMS = "http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd" - wfsCapabilitiesFilename = "/var/www/config/capabilities_wfs_200.xml" - wmsCapabilitiesFilename = "/var/www/config/capabilities_wms_130.xml" - metadataMediaType = "application/vnd.ogc.csw.GetRecordByIdResponse_xml" - XLinkURL = "http://www.w3.org/1999/xlink" -) - -func MapWFSToCapabilitiesGeneratorInput(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (*capabilitiesgenerator.Config, error) { - featureTypeList, err := getFeatureTypeList(wfs, ownerInfo) - if err != nil { - return nil, err - } - - config := capabilitiesgenerator.Config{ - Global: capabilitiesgenerator.Global{ - Namespace: mapperutils.GetNamespaceURI(wfs.Spec.Service.Prefix, ownerInfo), - Prefix: wfs.Spec.Service.Prefix, - Onlineresourceurl: wfs.URL().Scheme + "://" + wfs.URL().Host, - Path: wfs.URL().Path, - }, - Services: capabilitiesgenerator.Services{ - WFS200Config: &capabilitiesgenerator.WFS200Config{ - Filename: wfsCapabilitiesFilename, - Wfs200: wfs200.GetCapabilitiesResponse{ - - ServiceProvider: mapServiceProvider(&ownerInfo.Spec.WFS.ServiceProvider, ownerInfo.Spec.ProviderSite), - ServiceIdentification: wfs200.ServiceIdentification{ - Title: wfs.Spec.Service.Title, - Abstract: wfs.Spec.Service.Abstract, - AccessConstraints: wfs.Spec.Service.AccessConstraints.String(), - Keywords: &wsc110.Keywords{ - Keyword: wfs.Spec.Service.KeywordsIncludingInspireKeyword(), - }, - Fees: wfs.Spec.Service.Fees, - }, - Capabilities: wfs200.Capabilities{ - FeatureTypeList: *featureTypeList, - }, - }, - }, - }, - } - - if wfs.Spec.Service.Inspire != nil { - config.Global.AdditionalSchemaLocations = inspireSchemaLocationsWFS - var metadataURL wfs200.MetadataURL - if wfs.Spec.Service.Inspire.ServiceMetadataURL.CSW != nil { - metadataURL.URL, err = replaceMustacheTemplate(ownerInfo.Spec.MetadataUrls.CSW.HrefTemplate, wfs.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier) - if err != nil { - return nil, err - } - metadataURL.MediaType = metadataMediaType - } - - if wfs.Spec.Service.Inspire.ServiceMetadataURL.Custom != nil { - metadataURL.URL = wfs.Spec.Service.Inspire.ServiceMetadataURL.Custom.Href.String() - metadataURL.MediaType = wfs.Spec.Service.Inspire.ServiceMetadataURL.Custom.Type - } - - config.Services.WFS200Config.Wfs200.Capabilities.OperationsMetadata = &wfs200.OperationsMetadata{ - ExtendedCapabilities: &wfs200.ExtendedCapabilities{ - ExtendedCapabilities: wfs200.NestedExtendedCapabilities{ - MetadataURL: metadataURL, - SupportedLanguages: wfs200.SupportedLanguages{ - DefaultLanguage: wfs200.Language{ - Language: wfs.Spec.Service.Inspire.Language, - }, - }, - ResponseLanguage: wfs200.Language{Language: wfs.Spec.Service.Inspire.Language}, - SpatialDataSetIdentifier: wfs200.SpatialDataSetIdentifier{ - Code: wfs.Spec.Service.Inspire.SpatialDatasetIdentifier, - }, - }, - }, - } - } - if wfs.Spec.Service.CountDefault != nil { - operationsMetadata := config.Services.WFS200Config.Wfs200.Capabilities.OperationsMetadata - if operationsMetadata == nil { - operationsMetadata = &wfs200.OperationsMetadata{} - } - operationsMetadata.Constraint = getConstraints(strconv.Itoa(*wfs.Spec.Service.CountDefault)) - config.Services.WFS200Config.Wfs200.Capabilities.OperationsMetadata = operationsMetadata - } - - return &config, nil -} - -func getConstraints(countDefault string) []wfs200.Constraint { - return []wfs200.Constraint{ - { - Name: "ImplementsBasicWFS", - DefaultValue: ptr.To("TRUE"), - }, - { - Name: "ImplementsTransactionalWFS", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ImplementsLockingWFS", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "KVPEncoding", - DefaultValue: ptr.To("TRUE"), - }, - { - Name: "XMLEncoding", - DefaultValue: ptr.To("TRUE"), - }, - { - Name: "SOAPEncoding", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ImplementsInheritance", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ImplementsRemoteResolve", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ImplementsResultPaging", - DefaultValue: ptr.To("TRUE"), - }, - { - Name: "ImplementsStandardJoins", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ImplementsSpatialJoins", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ImplementsTemporalJoins", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ImplementsFeatureVersioning", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "ManageStoredQueries", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "PagingIsTransactionSafe", - DefaultValue: ptr.To("FALSE"), - }, - { - Name: "CountDefault", - DefaultValue: &countDefault, - }, - { - Name: "QueryExpressions", - AllowedValues: &wfs200.AllowedValues{Value: []string{ - "wfs:Query", - "wfs:StoredQuery", - }, - }, - }, - } -} - -func getFeatureTypeList(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (*wfs200.FeatureTypeList, error) { - typeList := wfs200.FeatureTypeList{} - - for _, fType := range wfs.Spec.Service.FeatureTypes { - defaultCRS, err := createCRSFromEpsgString(wfs.Spec.Service.DefaultCrs) - if err != nil { - return nil, err - } - - var otherCRS []*wfs200.CRS - for _, epsgString := range wfs.Spec.Service.OtherCrs { - CRS, err := createCRSFromEpsgString(epsgString) - if err != nil { - return nil, err - } - otherCRS = append(otherCRS, CRS) - } - - var wgs84BoundingBox *wsc110.WGS84BoundingBox - if fType.Bbox != nil && fType.Bbox.WGS84 != nil { - minX, err := strconv.ParseFloat(fType.Bbox.WGS84.MinX, 64) - if err != nil { - return nil, err - } - maxX, err := strconv.ParseFloat(fType.Bbox.WGS84.MaxX, 64) - if err != nil { - return nil, err - } - minY, err := strconv.ParseFloat(fType.Bbox.WGS84.MinY, 64) - if err != nil { - return nil, err - } - maxY, err := strconv.ParseFloat(fType.Bbox.WGS84.MaxY, 64) - if err != nil { - return nil, err - } - - wgs84BoundingBox = &wsc110.WGS84BoundingBox{ - LowerCorner: wsc110.Position{minX, minY}, - UpperCorner: wsc110.Position{maxX, maxY}, - } - } - - metadataURL, err := replaceMustacheTemplate(ownerInfo.Spec.MetadataUrls.CSW.HrefTemplate, fType.DatasetMetadataURL.CSW.MetadataIdentifier) - if err != nil { - return nil, err - } - - featureType := wfs200.FeatureType{ - Name: wfs.Spec.Service.Prefix + ":" + fType.Name, - Title: fType.Title, - Abstract: fType.Abstract, - Keywords: &[]wsc110.Keywords{ - { - Keyword: fType.Keywords, - }, - }, - MetadataURL: wfs200.MetadataHref{ - Href: metadataURL, - }, - DefaultCRS: defaultCRS, - OtherCRS: otherCRS, - WGS84BoundingBox: wgs84BoundingBox, - } - - typeList.FeatureType = append(typeList.FeatureType, featureType) - } - return &typeList, nil -} - -func createCRSFromEpsgString(epsgString string) (*wfs200.CRS, error) { - index := strings.LastIndex(epsgString, ":") - if index == -1 { - return nil, fmt.Errorf("could not determine EPSG code from EPSG string %s", epsgString) - } - epsgCodeString := epsgString[index+1:] - epsgCode, err := strconv.Atoi(epsgCodeString) - if err != nil { - return nil, fmt.Errorf("could not determine EPSG code from EPSG string %s", epsgCodeString) - } - - epsgUrn := "urn:ogc:def:crs:EPSG:" - - return &wfs200.CRS{ - Namespace: epsgUrn, - Code: epsgCode, - }, nil -} - -func replaceMustacheTemplate(hrefTemplate string, identifier string) (string, error) { - templateVariable := map[string]string{"identifier": identifier} - return mustache.Render(hrefTemplate, templateVariable) -} - -func mapServiceProvider(provider *smoothoperatorv1.ServiceProvider, providerSite *smoothoperatorv1.ProviderSite) (serviceProvider wfs200.ServiceProvider) { - if provider.ProviderName != nil { - serviceProvider.ProviderName = provider.ProviderName - } - - if providerSite != nil { - serviceProvider.ProviderSite = &wfs200.ProviderSite{ - Type: providerSite.Type, - Href: providerSite.Href, - } - } - - if provider.ServiceContact != nil { - serviceProvider.ServiceContact = &wfs200.ServiceContact{ - IndividualName: provider.ServiceContact.IndividualName, - PositionName: provider.ServiceContact.PositionName, - Role: provider.ServiceContact.Role, - } - if provider.ServiceContact.ContactInfo != nil { - serviceProvider.ServiceContact.ContactInfo = mapContactInfo(*provider.ServiceContact.ContactInfo) - } - } - - return serviceProvider -} - -func mapContactInfo(contactInfo smoothoperatorv1.ContactInfo) (serviceContactInfo *wfs200.ContactInfo) { - serviceContactInfo = &wfs200.ContactInfo{ - Text: contactInfo.Text, - HoursOfService: contactInfo.HoursOfService, - ContactInstructions: contactInfo.ContactInstructions, - } - if contactInfo.Phone != nil { - serviceContactInfo.Phone = &wfs200.Phone{ - Voice: contactInfo.Phone.Voice, - Facsimile: contactInfo.Phone.Facsimile, - } - } - if contactInfo.Address != nil { - serviceContactInfo.Address = &wfs200.Address{ - DeliveryPoint: contactInfo.Address.DeliveryPoint, - City: contactInfo.Address.City, - AdministrativeArea: contactInfo.Address.AdministrativeArea, - PostalCode: contactInfo.Address.PostalCode, - Country: contactInfo.Address.Country, - ElectronicMailAddress: contactInfo.Address.ElectronicMailAddress, - } - } - if contactInfo.OnlineResource != nil { - serviceContactInfo.OnlineResource = &wfs200.OnlineResource{ - Type: contactInfo.OnlineResource.Type, - Href: contactInfo.OnlineResource.Href, - } - } - return -} - -func MapWMSToCapabilitiesGeneratorInput(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (*capabilitiesgenerator.Config, error) { - canonicalServiceURL := wms.URL() - - layer, err := getLayers(wms, canonicalServiceURL.String()) - if err != nil { - return nil, err - } - - config := capabilitiesgenerator.Config{ - Global: capabilitiesgenerator.Global{ - // Prefix is unused for the WMS, but doesn't hurt to pass it - Namespace: mapperutils.GetNamespaceURI(wms.Spec.Service.Prefix, ownerInfo), - Prefix: wms.Spec.Service.Prefix, - Onlineresourceurl: wms.URL().Scheme + "://" + wms.URL().Host, - Path: wms.URL().Path, - }, - Services: capabilitiesgenerator.Services{ - WMS130Config: &capabilitiesgenerator.WMS130Config{ - Filename: wmsCapabilitiesFilename, - Wms130: wms130.GetCapabilitiesResponse{ - WMSService: wms130.WMSService{ - Name: "WMS", - Title: wms.Spec.Service.Title, - Abstract: &wms.Spec.Service.Abstract, - KeywordList: &wms130.Keywords{Keyword: wms.Spec.Service.KeywordsIncludingInspireKeyword()}, - OnlineResource: wms130.OnlineResource{Href: &ownerInfo.Spec.ProviderSite.Href}, - ContactInformation: getContactInformation(ownerInfo), - Fees: wms.Spec.Service.Fees, - AccessConstraints: ptr.To(wms.Spec.Service.AccessConstraints.String()), - OptionalConstraints: &wms130.OptionalConstraints{ - MaxWidth: int(smoothoperatorutils.PointerVal(wms.Spec.Service.MaxSize, 4000)), - MaxHeight: int(smoothoperatorutils.PointerVal(wms.Spec.Service.MaxSize, 4000)), - }, - }, - Capabilities: wms130.Capabilities{ - WMSCapabilities: wms130.WMSCapabilities{ - Request: wms130.Request{ - GetCapabilities: wms130.RequestType{ - Format: []string{"text/xml"}, - DCPType: getDcpType(canonicalServiceURL.String(), false), - }, - GetMap: wms130.RequestType{ - Format: []string{"image/png", "image/jpeg", "image/png; mode=8bit", "image/vnd.jpeg-png", "image/vnd.jpeg-png8"}, - DCPType: getDcpType(canonicalServiceURL.String(), true), - }, - GetFeatureInfo: &wms130.RequestType{ - Format: []string{"application/json", "application/json; subtype=geojson", "application/vnd.ogc.gml", "text/html", "text/plain", "text/xml", "text/xml; subtype=gml/3.1.1"}, - DCPType: getDcpType(canonicalServiceURL.String(), true), - }, - }, - Exception: wms130.ExceptionType{Format: []string{"XML", "INIMAGE", "BLANK"}}, - ExtendedCapabilities: nil, - Layer: layer, - }, - OptionalConstraints: nil, - }, - }, - }, - }, - } - - if wms.Spec.Service.Inspire != nil { - config.Global.AdditionalSchemaLocations = inspireSchemaLocationsWMS - metadataURL, _ := replaceMustacheTemplate(ownerInfo.Spec.MetadataUrls.CSW.HrefTemplate, wms.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier) - - defaultLanguage := wms130.Language{Language: wms.Spec.Service.Inspire.Language} - - config.Services.WMS130Config.Wms130.Capabilities.ExtendedCapabilities = &wms130.ExtendedCapabilities{ - MetadataURL: wms130.ExtendedMetadataURL{URL: metadataURL, MediaType: metadataMediaType}, - SupportedLanguages: wms130.SupportedLanguages{ - DefaultLanguage: defaultLanguage, - SupportedLanguage: &[]wms130.Language{defaultLanguage}, - }, - ResponseLanguage: defaultLanguage, - } - } - - return &config, nil -} - -func getContactInformation(ownerInfo *smoothoperatorv1.OwnerInfo) *wms130.ContactInformation { - result := wms130.ContactInformation{ - ContactPersonPrimary: nil, - ContactPosition: nil, - ContactAddress: nil, - ContactVoiceTelephone: nil, - ContactFacsimileTelephone: nil, - ContactElectronicMailAddress: nil, - } - - providedContactInformation := ownerInfo.Spec.WMS.ContactInformation - if providedContactInformation.ContactPersonPrimary != nil { - contactPerson := "" - if providedContactInformation.ContactPersonPrimary.ContactPerson != nil { - contactPerson = *providedContactInformation.ContactPersonPrimary.ContactPerson - } - contactOrganisation := "" - if providedContactInformation.ContactPersonPrimary.ContactOrganization != nil { - contactOrganisation = *providedContactInformation.ContactPersonPrimary.ContactOrganization - } - - contactPersonPrimary := wms130.ContactPersonPrimary{ - ContactPerson: contactPerson, - ContactOrganization: contactOrganisation, - } - result.ContactPersonPrimary = &contactPersonPrimary - } - - result.ContactPosition = providedContactInformation.ContactPosition - if providedContactInformation.ContactAddress != nil { - contactAddressInput := providedContactInformation.ContactAddress - contactAddress := wms130.ContactAddress{ - AddressType: smoothoperatorutils.PointerVal(contactAddressInput.AddressType, ""), - Address: smoothoperatorutils.PointerVal(contactAddressInput.Address, ""), - City: smoothoperatorutils.PointerVal(contactAddressInput.City, ""), - StateOrProvince: smoothoperatorutils.PointerVal(contactAddressInput.StateOrProvince, ""), - PostalCode: smoothoperatorutils.PointerVal(contactAddressInput.PostCode, ""), - Country: smoothoperatorutils.PointerVal(contactAddressInput.Country, ""), - } - result.ContactAddress = &contactAddress - } - - result.ContactVoiceTelephone = providedContactInformation.ContactVoiceTelephone - result.ContactFacsimileTelephone = providedContactInformation.ContactFacsimileTelephone - result.ContactElectronicMailAddress = providedContactInformation.ContactElectronicMailAddress - - return &result -} - -func getDcpType(url string, fillPost bool) *wms130.DCPType { - get := wms130.Method{ - OnlineResource: wms130.OnlineResource{ - Xlink: smoothoperatorutils.Pointer(XLinkURL), - Type: nil, - Href: smoothoperatorutils.Pointer(url + "?"), - }, - } - - var post *wms130.Method - if fillPost { - post = &wms130.Method{ - OnlineResource: wms130.OnlineResource{ - Xlink: smoothoperatorutils.Pointer(XLinkURL), - Type: nil, - Href: smoothoperatorutils.Pointer(url), - }, - } - } - - result := wms130.DCPType{ - HTTP: struct { - Get wms130.Method `xml:"Get" yaml:"get"` - Post *wms130.Method `xml:"Post" yaml:"post"` - }{ - Get: get, - Post: post, - }, - } - return &result -} - -func getLayers(wms *pdoknlv3.WMS, canonicalURL string) ([]wms130.Layer, error) { - layer, err := mapLayer(wms.Spec.Service.Layer, canonicalURL, nil, nil, nil, nil) - if err != nil { - return nil, err - } - return []wms130.Layer{*layer}, nil -} - -func mapLayer(layer pdoknlv3.Layer, canonicalURL string, authorityURL *wms130.AuthorityURL, identifier *wms130.Identifier, parentStyleNames []string, parentBBoxes []*wms130.LayerBoundingBox) (*wms130.Layer, error) { - if layer.Authority != nil { - authorityURL = &wms130.AuthorityURL{ - Name: layer.Authority.Name, - OnlineResource: wms130.OnlineResource{ - Xlink: nil, - Type: nil, - Href: &layer.Authority.URL, - }, - } - identifier = &wms130.Identifier{ - Authority: layer.Authority.Name, - Value: layer.Authority.SpatialDatasetIdentifier, - } - } - - crsses, exBbox, bboxes, err := mapBBoxes(layer.BoundingBoxes, parentBBoxes) - if err != nil { - return nil, err - } - - l := wms130.Layer{ - Queryable: smoothoperatorutils.Pointer(1), - Opaque: nil, - Name: layer.Name, - Title: smoothoperatorutils.PointerVal(layer.Title, ""), - Abstract: smoothoperatorutils.Pointer(smoothoperatorutils.PointerVal(layer.Abstract, "")), - KeywordList: &wms130.Keywords{Keyword: layer.Keywords}, - CRS: crsses, - EXGeographicBoundingBox: exBbox, - BoundingBox: bboxes, - Dimension: nil, - Attribution: nil, - AuthorityURL: authorityURL, - Identifier: identifier, - DataURL: nil, - FeatureListURL: nil, - Style: getLayerStyles(layer, canonicalURL, parentStyleNames), - Layer: []*wms130.Layer{}, - } - - if layer.MinScaleDenominator != nil { - float, err := strconv.ParseFloat(*layer.MinScaleDenominator, 64) - if err != nil { - return nil, err - } - l.MinScaleDenominator = &float - - } - - if layer.MaxScaleDenominator != nil { - float, err := strconv.ParseFloat(*layer.MaxScaleDenominator, 64) - if err != nil { - return nil, err - } - l.MaxScaleDenominator = &float - } - - if layer.DatasetMetadataURL != nil { - l.MetadataURL = append(l.MetadataURL, &wms130.MetadataURL{ - Type: smoothoperatorutils.Pointer("TC211"), - Format: smoothoperatorutils.Pointer("text/plain"), - OnlineResource: wms130.OnlineResource{ - Xlink: smoothoperatorutils.Pointer(XLinkURL), - Type: smoothoperatorutils.Pointer("simple"), - Href: smoothoperatorutils.Pointer("https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=" + layer.DatasetMetadataURL.CSW.MetadataIdentifier), - }, - }) - } - - layerStyleNames := []string{} - for _, s := range l.Style { - layerStyleNames = append(layerStyleNames, s.Name) - } - - // Map sublayers - for _, sublayer := range layer.Layers { - if sublayer.Visible { - mapped, err := mapLayer(sublayer, canonicalURL, authorityURL, identifier, append(parentStyleNames, layerStyleNames...), bboxes) - if err != nil { - return nil, err - } - l.Layer = append(l.Layer, mapped) - } - } - - return &l, nil -} - -func mapBBoxes(layerBBoxes []pdoknlv3.WMSBoundingBox, parentBBoxes []*wms130.LayerBoundingBox) ([]wms130.CRS, *wms130.EXGeographicBoundingBox, []*wms130.LayerBoundingBox, error) { - bboxMap := make(map[string]*wms130.LayerBoundingBox) - crsstrings := []string{} - for _, bbox := range parentBBoxes { - crsstrings = append(crsstrings, bbox.CRS) - bboxMap[bbox.CRS] = bbox - } - for _, bbox := range layerBBoxes { - minX, err := strconv.ParseFloat(bbox.BBox.MinX, 64) - if err != nil { - return nil, nil, nil, err - } - minY, err := strconv.ParseFloat(bbox.BBox.MinY, 64) - if err != nil { - return nil, nil, nil, err - } - maxX, err := strconv.ParseFloat(bbox.BBox.MaxX, 64) - if err != nil { - return nil, nil, nil, err - } - maxY, err := strconv.ParseFloat(bbox.BBox.MaxY, 64) - if err != nil { - return nil, nil, nil, err - } - if !slices.Contains(crsstrings, bbox.CRS) { - crsstrings = append(crsstrings, bbox.CRS) - } - bboxMap[bbox.CRS] = &wms130.LayerBoundingBox{ - CRS: bbox.CRS, - Minx: minX, - Miny: minY, - Maxx: maxX, - Maxy: maxY, - } - } - - var exBbox *wms130.EXGeographicBoundingBox - bboxes := []*wms130.LayerBoundingBox{} - crsses := []wms130.CRS{} - - for _, crs := range crsstrings { - crsSplit := strings.Split(crs, ":") - code, err := strconv.Atoi(crsSplit[1]) - if err != nil { - return nil, nil, nil, err - } - crsses = append(crsses, wms130.CRS{ - Namespace: crsSplit[0], - Code: code, - }) - - bbox := bboxMap[crs] - bboxes = append(bboxes, bbox) - - if crs == "CRS:84" { - exBbox = &wms130.EXGeographicBoundingBox{ - WestBoundLongitude: bbox.Minx, - EastBoundLongitude: bbox.Maxx, - SouthBoundLatitude: bbox.Miny, - NorthBoundLatitude: bbox.Maxy, - } - } - - } - return crsses, exBbox, bboxes, nil -} - -func getLayerStyles(layer pdoknlv3.Layer, canonicalURL string, parentStyleNames []string) (styles []*wms130.Style) { - for _, style := range layer.Styles { - if slices.Contains(parentStyleNames, style.Name) { - continue - } - - newStyle := wms130.Style{ - Name: style.Name, - Title: smoothoperatorutils.PointerVal(style.Title, ""), - Abstract: style.Abstract, - LegendURL: &wms130.LegendURL{ - Width: 78, - Height: 20, - Format: "image/png", - OnlineResource: wms130.OnlineResource{ - Xlink: smoothoperatorutils.Pointer(XLinkURL), - Type: smoothoperatorutils.Pointer("simple"), - Href: smoothoperatorutils.Pointer(canonicalURL + "/legend/" + *layer.Name + "/" + style.Name + ".png"), - }, - }, - StyleSheetURL: nil, - } - styles = append(styles, &newStyle) - } - return -} diff --git a/internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml b/internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml deleted file mode 100644 index 043d61f..0000000 --- a/internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml +++ /dev/null @@ -1,78 +0,0 @@ -global: - prefix: prefix - namespace: http://prefix.geonovum.nl - onlineResourceUrl: http://localhost - path: /datasetOwner/dataset/theme/wfs/v1_0 - additionalSchemaLocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd -services: - wfs200: - filename: /var/www/config/capabilities_wfs_200.xml - definition: - serviceIdentification: - title: some Service title - abstract: 'some "Service" abstract' - keywords: - keyword: - - service-keyword-1 - - service-keyword-2 - - infoFeatureAccessService - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - serviceProvider: - providerName: PDOK - capabilities: - operationsMetadata: - extendedCapabilities: - extendedCapabilities: - metadataUrl: - url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=metameta-meta-meta-meta-metametameta - mediaType: application/vnd.ogc.csw.GetRecordByIdResponse_xml - supportedLanguages: - defaultLanguage: - language: dut - responseLanguage: - language: dut - spatialDataSetIdentifier: - code: datadata-data-data-data-datadatadata - featureTypeList: - featureType: - - name: prefix:featuretype-1-name - title: featuretype-1-title - abstract: 'feature "1" abstract' - keywords: - - keyword: - - featuretype-1-keyword-1 - - featuretype-1-keyword-2 - defaultCrs: urn:ogc:def:crs:EPSG::28992 - otherCrs: - - urn:ogc:def:crs:EPSG::28992 - - urn:ogc:def:crs:EPSG::25831 - - urn:ogc:def:crs:EPSG::25832 - - urn:ogc:def:crs:EPSG::3034 - - urn:ogc:def:crs:EPSG::3035 - - urn:ogc:def:crs:EPSG::3857 - - urn:ogc:def:crs:EPSG::4258 - - urn:ogc:def:crs:EPSG::4326 - wgs84BoundingBox: - lowerCorner: "-180 -90" - upperCorner: "180 90" - metadataUrl: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - - name: prefix:featuretype-2-name - title: featuretype-2-title - abstract: 'feature "2" abstract' - keywords: - - keyword: - - featuretype-2-keyword-1 - - featuretype-2-keyword-2 - defaultCrs: urn:ogc:def:crs:EPSG::28992 - otherCrs: - - urn:ogc:def:crs:EPSG::28992 - - urn:ogc:def:crs:EPSG::25831 - - urn:ogc:def:crs:EPSG::25832 - - urn:ogc:def:crs:EPSG::3034 - - urn:ogc:def:crs:EPSG::3035 - - urn:ogc:def:crs:EPSG::3857 - - urn:ogc:def:crs:EPSG::4258 - - urn:ogc:def:crs:EPSG::4326 - metadataUrl: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata diff --git a/internal/controller/capabilitiesgenerator/test_data/wms_input.yaml b/internal/controller/capabilitiesgenerator/test_data/wms_input.yaml deleted file mode 100644 index 2b33553..0000000 --- a/internal/controller/capabilitiesgenerator/test_data/wms_input.yaml +++ /dev/null @@ -1,407 +0,0 @@ -global: - prefix: nwbwegen - namespace: http://nwbwegen.geonovum.nl - onlineResourceUrl: http://localhost - path: /rws/nwbwegen/wms/v1_0 - additionalSchemaLocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd -services: - wms130: - filename: /var/www/config/capabilities_wms_130.xml - definition: - wmsCapabilities: - space: "" - local: "" - service: - name: WMS - title: NWB - Wegen WMS - abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien van een straatnaam of nummer. - keywordList: - keyword: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - - Hectometerpunten - - HVD - - Mobiliteit - - infoMapAccessService - onlineResource: - xlink: null - type: null - href: https://www.pdok.nl - contactInformation: - contactPersonPrimary: - contactPerson: KlantContactCenter PDOK - contactOrganization: PDOK - contactPosition: pointOfContact - contactAddress: - addressType: Work - address: "" - city: Apeldoorn - stateOrProvince: "" - postalCode: "" - country: The Netherlands - contactVoiceTelephone: null - contactFacsimileTelephone: null - contactElectronicMailAddress: BeheerPDOK@kadaster.nl - accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - optionalConstraints: - maxWidth: 4000 - maxHeight: 4000 - capability: - wmsCapabilities: - request: - getCapabilities: - format: - - text/xml - dcpType: - http: - get: - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: null - href: http://localhost/rws/nwbwegen/wms/v1_0? - post: null - getMap: - format: - - image/png - - image/jpeg - - image/png; mode=8bit - - image/vnd.jpeg-png - - image/vnd.jpeg-png8 - dcpType: - http: - get: - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: null - href: http://localhost/rws/nwbwegen/wms/v1_0? - post: - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: null - href: http://localhost/rws/nwbwegen/wms/v1_0 - getFeatureInfo: - format: - - application/json - - application/json; subtype=geojson - - application/vnd.ogc.gml - - text/html - - text/plain - - text/xml - - text/xml; subtype=gml/3.1.1 - dcpType: - http: - get: - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: null - href: http://localhost/rws/nwbwegen/wms/v1_0? - post: - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: null - href: http://localhost/rws/nwbwegen/wms/v1_0 - exception: - format: - - XML - - INIMAGE - - BLANK - extendedCapabilities: - metadataUrl: - url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8 - mediaType: application/vnd.ogc.csw.GetRecordByIdResponse_xml - supportedLanguages: - defaultLanguage: - language: dut - supportedLanguage: - - language: dut - responseLanguage: - language: dut - layer: - - queryable: 1 - title: NWB - Wegen WMS - abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien van een straatnaam of nummer. - keywordList: - keyword: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - - Hectometerpunten - - HVD - - Mobiliteit - crs: - - EPSG:28992 - - EPSG:25831 - - EPSG:25832 - - EPSG:3034 - - EPSG:3035 - - EPSG:3857 - - EPSG:4258 - - EPSG:4326 - - CRS:84 - exGeographicBoundingBox: # without this Qgis cannot use the wms - westBoundLongitude: 2.52713 - eastBoundLongitude: 7.37403 - southBoundLatitude: 50.2129 - northBoundLatitude: 55.7212 - boundingBox: # without this Qgis cannot use the wms - - crs: EPSG:28992 - minx: -59188.44333693248 - miny: 304984.64144318487 - maxx: 308126.88473339565 - maxy: 858328.516489961 - - crs: EPSG:25831 - minx: -470271 - miny: 5.56231e+06 - maxx: 795163 - maxy: 6.18197e+06 - - crs: EPSG:25832 - minx: 62461.6 - miny: 5.56555e+06 - maxx: 397827 - maxy: 6.19042e+06 - - crs: EPSG:3034 - minx: 2.61336e+06 - miny: 3.509e+06 - maxx: 3.22007e+06 - maxy: 3.84003e+06 - - crs: EPSG:3035 - minx: 3.01676e+06 - miny: 3.81264e+06 - maxx: 3.64485e+06 - maxy: 4.15586e+06 - - crs: EPSG:3857 - minx: 281318 - miny: 6.48322e+06 - maxx: 820873 - maxy: 7.50311e+06 - - crs: EPSG:4258 - minx: 50.2129 - miny: 2.52713 - maxx: 55.7212 - maxy: 7.37403 - - crs: EPSG:4326 - minx: 50.2129 - miny: 2.52713 - maxx: 55.7212 - maxy: 7.37403 - - crs: CRS:84 - minx: 2.52713 - miny: 50.2129 - maxx: 7.37403 - maxy: 55.7212 - layer: - - queryable: 1 - name: wegvakken - title: Wegvakken - abstract: Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, routenummer, wegbeheerder, huisnummers, enz. weer. - keywordList: - keyword: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - crs: - - EPSG:28992 - - EPSG:25831 - - EPSG:25832 - - EPSG:3034 - - EPSG:3035 - - EPSG:3857 - - EPSG:4258 - - EPSG:4326 - - CRS:84 - exGeographicBoundingBox: # without this Qgis cannot use the wms - westBoundLongitude: 2.52713 - eastBoundLongitude: 7.37403 - southBoundLatitude: 50.2129 - northBoundLatitude: 55.7212 - boundingBox: # without this Qgis cannot use the wms - - crs: EPSG:28992 - minx: -59188.44333693248 - miny: 304984.64144318487 - maxx: 308126.88473339565 - maxy: 858328.516489961 - - crs: EPSG:25831 - minx: -470271 - miny: 5.56231e+06 - maxx: 795163 - maxy: 6.18197e+06 - - crs: EPSG:25832 - minx: 62461.6 - miny: 5.56555e+06 - maxx: 397827 - maxy: 6.19042e+06 - - crs: EPSG:3034 - minx: 2.61336e+06 - miny: 3.509e+06 - maxx: 3.22007e+06 - maxy: 3.84003e+06 - - crs: EPSG:3035 - minx: 3.01676e+06 - miny: 3.81264e+06 - maxx: 3.64485e+06 - maxy: 4.15586e+06 - - crs: EPSG:3857 - minx: 281318 - miny: 6.48322e+06 - maxx: 820873 - maxy: 7.50311e+06 - - crs: EPSG:4258 - minx: 50.2129 - miny: 2.52713 - maxx: 55.7212 - maxy: 7.37403 - - crs: EPSG:4326 - minx: 50.2129 - miny: 2.52713 - maxx: 55.7212 - maxy: 7.37403 - - crs: CRS:84 - minx: 2.52713 - miny: 50.2129 - maxx: 7.37403 - maxy: 55.7212 - authorityUrl: - name: rws - onlineResource: - href: https://www.rijkswaterstaat.nl - type: null - xlink: null - identifier: - authority: rws - value: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - metadataUrl: - - type: TC211 - format: text/plain - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: simple - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=a9b7026e-0a81-4813-93bd-ba49e6f28502 - style: - - name: wegvakken - title: NWB - Wegvakken - legendUrl: - width: 78 - height: 20 - format: image/png - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: simple - href: http://localhost/rws/nwbwegen/wms/v1_0/legend/wegvakken/wegvakken.png - minScaleDenominator: 1 - maxScaleDenominator: 50000 - - queryable: 1 - name: hectopunten - title: Hectopunten - abstract: Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, zijde en hectoletter weer. - keywordList: - keyword: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Hectometerpunten - crs: - - EPSG:28992 - - EPSG:25831 - - EPSG:25832 - - EPSG:3034 - - EPSG:3035 - - EPSG:3857 - - EPSG:4258 - - EPSG:4326 - - CRS:84 - exGeographicBoundingBox: # without this Qgis cannot use the wms - westBoundLongitude: 2.52713 - eastBoundLongitude: 7.37403 - southBoundLatitude: 50.2129 - northBoundLatitude: 55.7212 - boundingBox: # without this Qgis cannot use the wms - - crs: EPSG:28992 - minx: -59188.44333693248 - miny: 304984.64144318487 - maxx: 308126.88473339565 - maxy: 858328.516489961 - - crs: EPSG:25831 - minx: -470271 - miny: 5.56231e+06 - maxx: 795163 - maxy: 6.18197e+06 - - crs: EPSG:25832 - minx: 62461.6 - miny: 5.56555e+06 - maxx: 397827 - maxy: 6.19042e+06 - - crs: EPSG:3034 - minx: 2.61336e+06 - miny: 3.509e+06 - maxx: 3.22007e+06 - maxy: 3.84003e+06 - - crs: EPSG:3035 - minx: 3.01676e+06 - miny: 3.81264e+06 - maxx: 3.64485e+06 - maxy: 4.15586e+06 - - crs: EPSG:3857 - minx: 281318 - miny: 6.48322e+06 - maxx: 820873 - maxy: 7.50311e+06 - - crs: EPSG:4258 - minx: 50.2129 - miny: 2.52713 - maxx: 55.7212 - maxy: 7.37403 - - crs: EPSG:4326 - minx: 50.2129 - miny: 2.52713 - maxx: 55.7212 - maxy: 7.37403 - - crs: CRS:84 - minx: 2.52713 - miny: 50.2129 - maxx: 7.37403 - maxy: 55.7212 - authorityUrl: - name: rws - onlineResource: - href: https://www.rijkswaterstaat.nl - type: null - xlink: null - identifier: - authority: rws - value: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - metadataUrl: - - type: TC211 - format: text/plain - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: simple - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=a9b7026e-0a81-4813-93bd-ba49e6f28502 - style: - - name: hectopunten - title: NWB - Hectopunten - legendUrl: - width: 78 - height: 20 - format: image/png - onlineResource: - xlink: http://www.w3.org/1999/xlink - type: simple - href: http://localhost/rws/nwbwegen/wms/v1_0/legend/hectopunten/hectopunten.png - minScaleDenominator: 1 - maxScaleDenominator: 50000 diff --git a/internal/controller/configmaps.go b/internal/controller/configmaps.go deleted file mode 100644 index baea3fd..0000000 --- a/internal/controller/configmaps.go +++ /dev/null @@ -1,147 +0,0 @@ -package controller - -import ( - "fmt" - "strings" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/blobdownload" - "github.com/pdok/mapserver-operator/internal/controller/capabilitiesgenerator" - "github.com/pdok/mapserver-operator/internal/controller/mapfilegenerator" - "github.com/pdok/mapserver-operator/internal/controller/static" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" -) - -const ( - downloadScriptName = "gpkg_download.sh" - mapfileGeneratorInput = "input.json" - capabilitiesGeneratorInput = "input.yaml" -) - -func mutateConfigMapCapabilitiesGenerator[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap, ownerInfo *smoothoperatorv1.OwnerInfo) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { - return err - } - - if len(configMap.Data) == 0 { - input, err := capabilitiesgenerator.GetInput(obj, ownerInfo) - if err != nil { - return err - } - configMap.Data = map[string]string{capabilitiesGeneratorInput: input} - } - configMap.Immutable = smoothoperatorutils.Pointer(true) - - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { - return err - } - if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { - return err - } - return smoothoperatorutils.AddHashSuffix(configMap) -} - -func mutateConfigMapMapfileGenerator[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap, ownerInfo *smoothoperatorv1.OwnerInfo) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { - return err - } - - if len(configMap.Data) == 0 { - mapfileGeneratorConfig, err := mapfilegenerator.GetConfig(obj, ownerInfo) - if err != nil { - return err - } - configMap.Data = map[string]string{mapfileGeneratorInput: mapfileGeneratorConfig} - } - configMap.Immutable = smoothoperatorutils.Pointer(true) - - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { - return err - } - if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { - return err - } - return smoothoperatorutils.AddHashSuffix(configMap) -} - -func mutateConfigMapBlobDownload[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { - return err - } - - if len(configMap.Data) == 0 { - downloadScript := blobdownload.GetScript() - configMap.Data = map[string]string{downloadScriptName: downloadScript} - } - configMap.Immutable = smoothoperatorutils.Pointer(true) - - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { - return err - } - if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { - return err - } - return smoothoperatorutils.AddHashSuffix(configMap) -} - -func getBareConfigMap[O pdoknlv3.WMSWFS](obj O, name string) *corev1.ConfigMap { - return &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: getSuffixedName(obj, name), - Namespace: obj.GetNamespace(), - }, - } -} - -func mutateConfigMap[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { - return err - } - - configMap.Immutable = smoothoperatorutils.Pointer(true) - configMap.Data = map[string]string{} - - updateConfigMapWithStaticFiles(configMap, obj) - - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { - return err - } - if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { - return err - } - return smoothoperatorutils.AddHashSuffix(configMap) -} - -func updateConfigMapWithStaticFiles[O pdoknlv3.WMSWFS](configMap *corev1.ConfigMap, obj O) { - staticFileName, contents := static.GetStaticFiles() - for _, name := range staticFileName { - content := contents[name] - if name == "include.conf" { - ingressRouteUrls := obj.IngressRouteURLs(true) - rewriteRules := make([]string, 0) - for _, ingressRouteURL := range ingressRouteUrls { - rewriteRules = append(rewriteRules, fmt.Sprintf(" \"%s/legend(.*)\" => \"/legend$1\"", ingressRouteURL.URL.Path)) - rewriteRules = append(rewriteRules, fmt.Sprintf(" \"%s(.*)\" => \"/mapserver$1\"", ingressRouteURL.URL.Path)) - } - - content = []byte(strings.ReplaceAll(string(content), "{{ rewrite_rules }}", strings.Join(rewriteRules, ",\n"))) - } - configMap.Data[name] = string(content) - } -} diff --git a/internal/controller/configmaps_test.go b/internal/controller/configmaps_test.go deleted file mode 100644 index 143a467..0000000 --- a/internal/controller/configmaps_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package controller - -import ( - "os" - "testing" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/constants" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - "sigs.k8s.io/yaml" -) - -func TestMapserverConfigMaps(t *testing.T) { - wfsBytes, err := os.ReadFile("test_data/wfs/complete/input/wfs.yaml") - assert.NoError(t, err) - o := &pdoknlv3.WFS{} - err = yaml.Unmarshal(wfsBytes, o) - assert.NoError(t, err) - generatedConfigMap := getBareConfigMap(o, constants.MapserverName) - generatedConfigMap.Data = make(map[string]string) - updateConfigMapWithStaticFiles(generatedConfigMap, o) - - expectedConfigMap := v1.ConfigMap{} - expectedBytes, err := os.ReadFile("test_data/wfs/complete/expected/configmap-mapserver.yaml") - assert.NoError(t, err) - err = yaml.Unmarshal(expectedBytes, &expectedConfigMap) - assert.NoError(t, err) - - assert.Equal(t, expectedConfigMap.Data, generatedConfigMap.Data) -} diff --git a/internal/controller/constants/constants.go b/internal/controller/constants/constants.go deleted file mode 100644 index 8e9eb6c..0000000 --- a/internal/controller/constants/constants.go +++ /dev/null @@ -1,29 +0,0 @@ -package constants - -const ( - MapserverName = "mapserver" - OgcWebserviceProxyName = "ogc-webservice-proxy" - MapfileGeneratorName = "mapfile-generator" - CapabilitiesGeneratorName = "capabilities-generator" - BlobDownloadName = "blob-download" - InitScriptsName = "init-scripts" - LegendGeneratorName = "legend-generator" - LegendFixerName = "legend-fixer" - FeatureinfoGeneratorName = "featureinfo-generator" - - BaseVolumeName = "base" - DataVolumeName = "data" - - configSuffix = "-config" - ConfigMapMapfileGeneratorVolumeName = MapfileGeneratorName + configSuffix - ConfigMapStylingFilesVolumeName = "styling-files" - ConfigMapCapabilitiesGeneratorVolumeName = CapabilitiesGeneratorName + configSuffix - ConfigMapOgcWebserviceProxyVolumeName = OgcWebserviceProxyName + configSuffix - ConfigMapLegendGeneratorVolumeName = LegendGeneratorName + configSuffix - ConfigMapFeatureinfoGeneratorVolumeName = FeatureinfoGeneratorName + configSuffix - ConfigMapCustomMapfileVolumeName = "mapfile" - - HTMLTemplatesPath = "/srv/data/config/templates" - MapserverPortNr int32 = 80 - ApachePortNr int32 = 9117 -) diff --git a/internal/controller/deployment.go b/internal/controller/deployment.go deleted file mode 100644 index 97be2c6..0000000 --- a/internal/controller/deployment.go +++ /dev/null @@ -1,296 +0,0 @@ -package controller - -import ( - "strconv" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/blobdownload" - "github.com/pdok/mapserver-operator/internal/controller/capabilitiesgenerator" - "github.com/pdok/mapserver-operator/internal/controller/constants" - "github.com/pdok/mapserver-operator/internal/controller/featureinfogenerator" - "github.com/pdok/mapserver-operator/internal/controller/legendgenerator" - "github.com/pdok/mapserver-operator/internal/controller/mapfilegenerator" - "github.com/pdok/mapserver-operator/internal/controller/mapperutils" - "github.com/pdok/mapserver-operator/internal/controller/mapserver" - "github.com/pdok/mapserver-operator/internal/controller/ogcwebserviceproxy" - "github.com/pdok/mapserver-operator/internal/controller/types" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - ctrl "sigs.k8s.io/controller-runtime" -) - -var storageClassName string - -func SetStorageClassName(name string) { - storageClassName = name -} - -func getBareDeployment[O pdoknlv3.WMSWFS](obj O) *appsv1.Deployment { - return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: getSuffixedName(obj, constants.MapserverName), - // name might become too long. not handling here. will just fail on apply. - Namespace: obj.GetNamespace(), - }, - } -} - -func mutateDeployment[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, deployment *appsv1.Deployment, configMapNames types.HashedConfigMapNames) error { - reconcilerClient := getReconcilerClient(r) - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, deployment, labels); err != nil { - return err - } - - deployment.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels} - - deployment.Spec.RevisionHistoryLimit = smoothoperatorutils.Pointer(int32(1)) - deployment.Spec.Strategy = appsv1.DeploymentStrategy{ - Type: appsv1.RollingUpdateDeploymentStrategyType, - RollingUpdate: &appsv1.RollingUpdateDeployment{ - MaxUnavailable: &intstr.IntOrString{IntVal: 1}, - MaxSurge: &intstr.IntOrString{IntVal: 1}, - }, - } - - initContainers, err := getInitContainerForDeployment(r, obj) - if err != nil { - return err - } - setTerminationMessage(initContainers) - - images := getReconcilerImages(r) - containers, err := getContainers(obj, images) - if err != nil { - return err - } - setTerminationMessage(containers) - - volumes := getVolumes(obj, configMapNames) - - podTemplateSpec := corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: getPodAnnotations(deployment), - Labels: labels, - }, - Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyAlways, - DNSPolicy: corev1.DNSClusterFirst, - TerminationGracePeriodSeconds: smoothoperatorutils.Pointer(int64(60)), - InitContainers: initContainers, - Containers: containers, - Volumes: volumes, - }, - } - - podPatch := obj.PodSpecPatch() - patchedSpec, err := smoothoperatorutils.StrategicMergePatch(&podTemplateSpec.Spec, &podPatch) - if err != nil { - return err - } - podTemplateSpec.Spec = *patchedSpec - - if use, _ := mapperutils.UseEphemeralVolume(obj); !use { - ephStorage := podTemplateSpec.Spec.Containers[0].Resources.Limits[corev1.ResourceEphemeralStorage] - threshold := resource.MustParse("200M") - - if ephStorage.Value() < threshold.Value() { - podTemplateSpec.Spec.Containers[0].Resources.Limits[corev1.ResourceEphemeralStorage] = threshold - } - } else { - delete(podTemplateSpec.Spec.Containers[0].Resources.Limits, corev1.ResourceEphemeralStorage) - delete(podTemplateSpec.Spec.Containers[0].Resources.Requests, corev1.ResourceEphemeralStorage) - } - - deployment.Spec.Template = podTemplateSpec - - if err = smoothoperatorutils.EnsureSetGVK(reconcilerClient, deployment, deployment); err != nil { - return err - } - return ctrl.SetControllerReference(obj, deployment, getReconcilerScheme(r)) -} - -func getPodAnnotations(deployment *appsv1.Deployment) map[string]string { - annotations := smoothoperatorutils.CloneOrEmptyMap(deployment.Spec.Template.GetAnnotations()) - annotations["cluster-autoscaler.kubernetes.io/safe-to-evict"] = "true" - annotations["kubectl.kubernetes.io/default-container"] = constants.MapserverName - annotations["match-regex.version-checker.io/mapserver"] = `^\d\.\d\.\d.*$` - annotations["prometheus.io/scrape"] = "true" - annotations["prometheus.io/port"] = strconv.Itoa(int(constants.ApachePortNr)) - annotations["priority.version-checker.io/mapserver"] = "4" - annotations["priority.version-checker.io/ogc-webservice-proxy"] = "4" - return annotations -} - -func getInitContainerForDeployment[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O) ([]corev1.Container, error) { - - images := getReconcilerImages(r) - blobDownloadInitContainer, err := blobdownload.GetBlobDownloadInitContainer(obj, *images) - if err != nil { - return nil, err - } - capabilitiesGeneratorInitContainer, err := capabilitiesgenerator.GetCapabilitiesGeneratorInitContainer(obj, *images) - if err != nil { - return nil, err - } - - initContainers := []corev1.Container{ - *blobDownloadInitContainer, - *capabilitiesGeneratorInitContainer, - } - - if obj.Mapfile() == nil { - mapfileGeneratorInitContainer, err := mapfilegenerator.GetMapfileGeneratorInitContainer(obj, *images) - if err != nil { - return nil, err - } - initContainers = append(initContainers, *mapfileGeneratorInitContainer) - } - - if wms, ok := any(obj).(*pdoknlv3.WMS); ok { - featureInfoInitContainer, err := featureinfogenerator.GetFeatureinfoGeneratorInitContainer(*images) - if err != nil { - return nil, err - } - initContainers = append(initContainers, *featureInfoInitContainer) - - legendGeneratorInitContainer, err := legendgenerator.GetLegendGeneratorInitContainer(wms, *images) - if err != nil { - return nil, err - } - initContainers = append(initContainers, *legendGeneratorInitContainer) - - if wms.Options().RewriteGroupToDataLayers { - legendFixerInitContainer := legendgenerator.GetLegendFixerInitContainer(*images) - initContainers = append(initContainers, *legendFixerInitContainer) - } - - } - return initContainers, nil -} - -func getContainers[O pdoknlv3.WMSWFS](obj O, images *types.Images) ([]corev1.Container, error) { - mapserverContainer, err := mapserver.GetMapserverContainer(obj, *images) - if err != nil { - return nil, err - } - containers := []corev1.Container{ - *mapserverContainer, - getApacheContainer(*images), - } - if wms, ok := any(obj).(*pdoknlv3.WMS); ok { - ogcProxy, err := ogcwebserviceproxy.GetOgcWebserviceProxyContainer(wms, *images) - if err != nil { - return nil, err - } - containers = append(containers, *ogcProxy) - } - return containers, nil -} - -func getApacheContainer(images types.Images) corev1.Container { - return corev1.Container{ - Name: "apache-exporter", - Image: images.ApacheExporterImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Ports: []corev1.ContainerPort{{ContainerPort: constants.ApachePortNr, Protocol: corev1.ProtocolTCP}}, - Args: []string{"--scrape_uri=http://localhost/server-status?auto"}, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("48M")}, - Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("0.02")}, - }, - } -} - -func setTerminationMessage(c []corev1.Container) { - for i := range c { - c[i].TerminationMessagePolicy = "File" - c[i].TerminationMessagePath = "/dev/termination-log" - } -} - -func getVolumes[O pdoknlv3.WMSWFS](obj O, configMapNames types.HashedConfigMapNames) []corev1.Volume { - baseVolume := corev1.Volume{Name: constants.BaseVolumeName} - if use, size := mapperutils.UseEphemeralVolume(obj); use { - baseVolume.Ephemeral = &corev1.EphemeralVolumeSource{ - VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{ - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, - Resources: corev1.VolumeResourceRequirements{Requests: corev1.ResourceList{ - corev1.ResourceStorage: *size, - }}, - }, - }, - } - if storageClassName != "" { - baseVolume.Ephemeral.VolumeClaimTemplate.Spec.StorageClassName = &storageClassName - } - } else { - baseVolume.EmptyDir = &corev1.EmptyDirVolumeSource{} - } - - volumes := []corev1.Volume{ - baseVolume, - {Name: constants.DataVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, - getConfigMapVolume(constants.MapserverName, configMapNames.Mapserver), - } - - if mapfile := obj.Mapfile(); mapfile != nil { - volumes = append(volumes, getConfigMapVolume(constants.ConfigMapCustomMapfileVolumeName, mapfile.ConfigMapKeyRef.Name)) - } - - if obj.Type() == pdoknlv3.ServiceTypeWMS && obj.Options().UseWebserviceProxy() { - volumes = append(volumes, getConfigMapVolume(constants.ConfigMapOgcWebserviceProxyVolumeName, configMapNames.OgcWebserviceProxy)) - } - - if obj.Options().PrefetchData { - vol := getConfigMapVolume(constants.InitScriptsName, configMapNames.InitScripts) - vol.ConfigMap.DefaultMode = smoothoperatorutils.Pointer(int32(0777)) - volumes = append(volumes, vol) - } - - volumes = append(volumes, getConfigMapVolume(constants.ConfigMapCapabilitiesGeneratorVolumeName, configMapNames.CapabilitiesGenerator)) - - if obj.Mapfile() == nil { - volumes = append(volumes, getConfigMapVolume(constants.ConfigMapMapfileGeneratorVolumeName, configMapNames.MapfileGenerator)) - } - - if obj.Type() == pdoknlv3.ServiceTypeWMS { - if obj.Mapfile() == nil { - wms, _ := any(obj).(*pdoknlv3.WMS) - volumeProjections := []corev1.VolumeProjection{} - for _, cm := range wms.Spec.Service.StylingAssets.ConfigMapRefs { - volumeProjections = append(volumeProjections, corev1.VolumeProjection{ - ConfigMap: &corev1.ConfigMapProjection{LocalObjectReference: corev1.LocalObjectReference{Name: cm.Name}}, - }) - } - - volumes = append(volumes, corev1.Volume{ - Name: constants.ConfigMapStylingFilesVolumeName, - VolumeSource: corev1.VolumeSource{Projected: &corev1.ProjectedVolumeSource{Sources: volumeProjections}}, - }) - } - - volumes = append( - volumes, - getConfigMapVolume(constants.ConfigMapFeatureinfoGeneratorVolumeName, configMapNames.FeatureInfoGenerator), - getConfigMapVolume(constants.ConfigMapLegendGeneratorVolumeName, configMapNames.LegendGenerator), - ) - } - - return volumes -} - -func getConfigMapVolume(name, configMap string) corev1.Volume { - return corev1.Volume{ - Name: name, - VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ - DefaultMode: smoothoperatorutils.Pointer(int32(0644)), - LocalObjectReference: corev1.LocalObjectReference{Name: configMap}}, - }, - } -} diff --git a/internal/controller/featureinfogenerator/featureinfo_generator.go b/internal/controller/featureinfogenerator/featureinfo_generator.go deleted file mode 100644 index d1fe3b1..0000000 --- a/internal/controller/featureinfogenerator/featureinfo_generator.go +++ /dev/null @@ -1,49 +0,0 @@ -package featureinfogenerator - -import ( - "encoding/json" - "fmt" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/types" - "github.com/pdok/mapserver-operator/internal/controller/utils" - corev1 "k8s.io/api/core/v1" -) - -func GetFeatureinfoGeneratorInitContainer(images types.Images) (*corev1.Container, error) { - initContainer := corev1.Container{ - Name: constants.FeatureinfoGeneratorName, - Image: images.FeatureinfoGeneratorImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{constants.FeatureinfoGeneratorName}, - Args: []string{ - "--input-path", - "/input/input.json", - "--dest-folder", - constants.HTMLTemplatesPath, - "--file-name", - "feature-info", - }, - VolumeMounts: []corev1.VolumeMount{ - utils.GetBaseVolumeMount(), - utils.GetConfigVolumeMount(constants.ConfigMapFeatureinfoGeneratorVolumeName), - }, - } - - return &initContainer, nil -} - -func GetInput(wms *pdoknlv3.WMS) (string, error) { - input, err := MapWMSToFeatureinfoGeneratorInput(wms) - if err != nil { - return "", err - } - jsonInput, err := json.MarshalIndent(input, "", " ") - if err != nil { - return "", fmt.Errorf("failed to marshal the featureinfo generator input to json: %w", err) - } - - return string(jsonInput), nil -} diff --git a/internal/controller/featureinfogenerator/featureinfo_generator_test.go b/internal/controller/featureinfogenerator/featureinfo_generator_test.go deleted file mode 100644 index 5fb0045..0000000 --- a/internal/controller/featureinfogenerator/featureinfo_generator_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package featureinfogenerator - -import ( - "testing" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -const ( - featureInfoGeneratorInput = `{ - "automaticCasing": true, - "version": 2, - "layers": [ - { - "name": "gpkg-layer-name", - "groupName": "group-layer-name", - "properties": [ - { - "name": "fuuid" - }, - { - "name": "column-1", - "alias": "ALIAS_column-1" - }, - { - "name": "column-2" - } - ] - }, - { - "name": "postgis-layer-name", - "groupName": "group-layer-name", - "properties": [ - { - "name": "fuuid" - }, - { - "name": "column-1" - }, - { - "name": "column-2" - } - ] - }, - { - "name": "tif-layer-name", - "groupName": "group-layer-name", - "properties": [ - { - "name": "value_list" - }, - { - "name": "class" - } - ] - } - ] -}` -) - -func TestGetInput(t *testing.T) { - type args struct { - wms *pdoknlv3.WMS - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "GetInput for featureinfoGenerator", - args: args{ - wms: &pdoknlv3.WMS{ - Spec: pdoknlv3.WMSSpec{ - Options: &pdoknlv3.Options{ - BaseOptions: pdoknlv3.BaseOptions{AutomaticCasing: true}, - }, - Service: pdoknlv3.WMSService{ - Layer: pdoknlv3.Layer{ - Name: smoothoperatorutils.Pointer("top-layer-name"), - Layers: []pdoknlv3.Layer{ - { - Name: smoothoperatorutils.Pointer("group-layer-name"), - Layers: []pdoknlv3.Layer{ - { - Name: smoothoperatorutils.Pointer("gpkg-layer-name"), - Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ - Gpkg: &pdoknlv3.Gpkg{ - Columns: []pdoknlv3.Column{ - {Name: "column-1", Alias: smoothoperatorutils.Pointer("ALIAS_column-1")}, - {Name: "column-2"}, - }, - }}, - }, - }, - { - Name: smoothoperatorutils.Pointer("postgis-layer-name"), - Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ - Postgis: &pdoknlv3.Postgis{ - Columns: []pdoknlv3.Column{ - {Name: "column-1"}, - {Name: "column-2"}, - }, - }}, - }, - }, - { - Name: smoothoperatorutils.Pointer("tif-layer-name"), - Data: &pdoknlv3.Data{ - TIF: &pdoknlv3.TIF{ - GetFeatureInfoIncludesClass: true, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: featureInfoGeneratorInput, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := GetInput(tt.args.wms) - if (err != nil) != tt.wantErr { - t.Errorf("GetInput() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("GetInput() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/internal/controller/featureinfogenerator/mapper.go b/internal/controller/featureinfogenerator/mapper.go deleted file mode 100644 index 188ddcd..0000000 --- a/internal/controller/featureinfogenerator/mapper.go +++ /dev/null @@ -1,69 +0,0 @@ -package featureinfogenerator - -import ( - featureinfo "github.com/pdok/featureinfo-generator/pkg/types" - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -const ( - featureinfoGeneratorSchemaVersion = 2 -) - -func MapWMSToFeatureinfoGeneratorInput(wms *pdoknlv3.WMS) (*featureinfo.Scheme, error) { - - input := &featureinfo.Scheme{ - AutomaticCasing: wms.Options().AutomaticCasing, - Version: featureinfoGeneratorSchemaVersion, - Layers: []featureinfo.Layer{}, - } - - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if !layer.IsDataLayer { - continue - } - l := featureinfo.Layer{ - Name: *layer.Name, - Properties: getProperties(&layer.Layer), - } - - if layer.GroupName != nil && layer.GroupName != wms.Spec.Service.Layer.Name { - l.GroupName = smoothoperatorutils.PointerVal(layer.GroupName, "") - } - - input.Layers = append(input.Layers, l) - } - return input, nil -} - -func getProperties(layer *pdoknlv3.Layer) (properties []featureinfo.Property) { - switch { - case layer.Data.Gpkg != nil: - properties = getPropertiesForVector(layer.Data.Gpkg.Columns) - case layer.Data.Postgis != nil: - properties = getPropertiesForVector(layer.Data.Postgis.Columns) - case layer.Data.TIF != nil: - properties = getPropertiesForRaster(&layer.Data.TIF.GetFeatureInfoIncludesClass) - } - return -} - -func getPropertiesForVector(columns []pdoknlv3.Column) (properties []featureinfo.Property) { - properties = append(properties, featureinfo.Property{Name: "fuuid"}) - for _, column := range columns { - prop := featureinfo.Property{Name: column.Name} - if column.Alias != nil { - prop.Alias = *column.Alias - } - properties = append(properties, prop) - } - return -} - -func getPropertiesForRaster(includeClass *bool) (properties []featureinfo.Property) { - properties = append(properties, featureinfo.Property{Name: "value_list"}) - if includeClass != nil && *includeClass { - properties = append(properties, featureinfo.Property{Name: "class"}) - } - return -} diff --git a/internal/controller/horizontalpodautoscaler.go b/internal/controller/horizontalpodautoscaler.go deleted file mode 100644 index d5cb2bb..0000000 --- a/internal/controller/horizontalpodautoscaler.go +++ /dev/null @@ -1,97 +0,0 @@ -package controller - -import ( - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/constants" - "github.com/pdok/mapserver-operator/internal/controller/mapperutils" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - appsv1 "k8s.io/api/apps/v1" - autoscalingv2 "k8s.io/api/autoscaling/v2" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" -) - -func mutateHorizontalPodAutoscaler[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, autoscaler *autoscalingv2.HorizontalPodAutoscaler) error { - reconcilerClient := getReconcilerClient(r) - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, autoscaler, labels); err != nil { - return err - } - - autoscaler.Spec.MaxReplicas = 30 - autoscaler.Spec.MinReplicas = smoothoperatorutils.Pointer(int32(2)) - autoscaler.Spec.ScaleTargetRef = autoscalingv2.CrossVersionObjectReference{ - APIVersion: appsv1.SchemeGroupVersion.String(), - Kind: "Deployment", - Name: getSuffixedName(obj, constants.MapserverName), - } - - var averageCPU int32 = 90 - if cpu := mapperutils.GetContainerResourceRequest(obj, constants.MapserverName, corev1.ResourceCPU); cpu != nil { - averageCPU = 80 - } - autoscaler.Spec.Metrics = []autoscalingv2.MetricSpec{{ - Type: autoscalingv2.ResourceMetricSourceType, - Resource: &autoscalingv2.ResourceMetricSource{ - Name: corev1.ResourceCPU, - Target: autoscalingv2.MetricTarget{ - Type: autoscalingv2.UtilizationMetricType, - AverageUtilization: &averageCPU, - }, - }, - }} - - var behaviourStabilizationWindowSeconds int32 - if obj.Type() == pdoknlv3.ServiceTypeWFS { - behaviourStabilizationWindowSeconds = 300 - } - - autoscaler.Spec.Behavior = &autoscalingv2.HorizontalPodAutoscalerBehavior{ - ScaleUp: &autoscalingv2.HPAScalingRules{ - StabilizationWindowSeconds: &behaviourStabilizationWindowSeconds, - Policies: []autoscalingv2.HPAScalingPolicy{{ - Type: autoscalingv2.PodsScalingPolicy, - Value: 20, - PeriodSeconds: 60, - }}, - SelectPolicy: smoothoperatorutils.Pointer(autoscalingv2.MaxChangePolicySelect), - }, - ScaleDown: &autoscalingv2.HPAScalingRules{ - StabilizationWindowSeconds: smoothoperatorutils.Pointer(int32(3600)), - Policies: []autoscalingv2.HPAScalingPolicy{ - { - Type: autoscalingv2.PercentScalingPolicy, - Value: 10, - PeriodSeconds: 600, - }, - { - Type: autoscalingv2.PodsScalingPolicy, - Value: 1, - PeriodSeconds: 600, - }, - }, - SelectPolicy: smoothoperatorutils.Pointer(autoscalingv2.MaxChangePolicySelect), - }, - } - if obj.HorizontalPodAutoscalerPatch() != nil { - patchedSpec, err := smoothoperatorutils.StrategicMergePatch(&autoscaler.Spec, obj.HorizontalPodAutoscalerPatch()) - if err != nil { - return err - } - autoscaler.Spec = *patchedSpec - } - if err := smoothoperatorutils.EnsureSetGVK(getReconcilerClient(r), autoscaler, autoscaler); err != nil { - return err - } - return ctrl.SetControllerReference(obj, autoscaler, getReconcilerScheme(r)) -} - -func getBareHorizontalPodAutoScaler[O pdoknlv3.WMSWFS](obj O) *autoscalingv2.HorizontalPodAutoscaler { - return &autoscalingv2.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: getSuffixedName(obj, constants.MapserverName), - Namespace: obj.GetNamespace(), - }, - } -} diff --git a/internal/controller/ingressroute.go b/internal/controller/ingressroute.go deleted file mode 100644 index 4863aa4..0000000 --- a/internal/controller/ingressroute.go +++ /dev/null @@ -1,161 +0,0 @@ -package controller - -import ( - "regexp" - "strings" - - smoothoperatormodel "github.com/pdok/smooth-operator/model" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - "github.com/pdok/mapserver-operator/internal/controller/utils" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - ctrl "sigs.k8s.io/controller-runtime" -) - -var setUptimeOperatorAnnotations = true - -func SetUptimeOperatorAnnotations(set bool) { - setUptimeOperatorAnnotations = set -} - -func getBareIngressRoute[O pdoknlv3.WMSWFS](obj O) *traefikiov1alpha1.IngressRoute { - return &traefikiov1alpha1.IngressRoute{ - ObjectMeta: metav1.ObjectMeta{ - Name: getSuffixedName(obj, constants.MapserverName), - Namespace: obj.GetNamespace(), - }, - } -} - -func mutateIngressRoute[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, ingressRoute *traefikiov1alpha1.IngressRoute) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, ingressRoute, labels); err != nil { - return err - } - - annotations := smoothoperatorutils.CloneOrEmptyMap(obj.GetAnnotations()) - if setUptimeOperatorAnnotations { - tags := []string{"public-stats", strings.ToLower(string(obj.Type()))} - - if obj.Inspire() != nil { - tags = append(tags, "inspire") - } - - queryString, _, err := obj.ReadinessQueryString() - if err != nil { - return err - } - - annotations["uptime.pdok.nl/id"] = utils.Sha1Hash(obj.TypedName()) - annotations["uptime.pdok.nl/name"] = getUptimeName(obj) - annotations["uptime.pdok.nl/url"] = obj.URL().String() + "?" + queryString - annotations["uptime.pdok.nl/tags"] = strings.Join(tags, ",") - } - ingressRoute.SetAnnotations(annotations) - - mapserverService := traefikiov1alpha1.Service{ - LoadBalancerSpec: traefikiov1alpha1.LoadBalancerSpec{ - Name: getBareService(obj).GetName(), - Kind: "Service", - Port: intstr.IntOrString{ - Type: intstr.Int, - IntVal: constants.MapserverPortNr, - }, - }, - } - - webServiceProxyService := traefikiov1alpha1.Service{ - LoadBalancerSpec: traefikiov1alpha1.LoadBalancerSpec{ - Name: getBareService(obj).GetName(), - Kind: "Service", - Port: intstr.IntOrString{ - Type: intstr.Int, - IntVal: int32(mapserverWebserviceProxyPortNr), - }, - }, - } - - middlewareRef := traefikiov1alpha1.MiddlewareRef{ - Name: getBareCorsHeadersMiddleware(obj).GetName(), - } - - makeRoute := func(match string, service traefikiov1alpha1.Service, middlewareRef traefikiov1alpha1.MiddlewareRef) traefikiov1alpha1.Route { - return traefikiov1alpha1.Route{ - Kind: "Rule", - Match: match, - Services: []traefikiov1alpha1.Service{service}, - Middlewares: []traefikiov1alpha1.MiddlewareRef{middlewareRef}, - } - } - - ingressRoute.Spec.Routes = []traefikiov1alpha1.Route{} - if obj.Type() == pdoknlv3.ServiceTypeWMS { - for _, ingressRouteURL := range obj.IngressRouteURLs(true) { - ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getLegendMatchRule(ingressRouteURL.URL), mapserverService, middlewareRef)) - - if obj.Options().UseWebserviceProxy() { - ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getMatchRule(ingressRouteURL.URL), webServiceProxyService, middlewareRef)) - } else { - ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getMatchRule(ingressRouteURL.URL), mapserverService, middlewareRef)) - } - } - } else { // WFS - for _, ingressRouteURL := range obj.IngressRouteURLs(true) { - ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getMatchRule(ingressRouteURL.URL), mapserverService, middlewareRef)) - } - } - - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, ingressRoute, ingressRoute); err != nil { - return err - } - return ctrl.SetControllerReference(obj, ingressRoute, getReconcilerScheme(r)) -} - -// getUptimeName transforms the CR name into a uptime.pdok.nl/name value -// owner-dataset-v1-0 -> OWNER dataset v1_0 [INSPIRE] [WMS|WFS] -func getUptimeName[O pdoknlv3.WMSWFS](obj O) string { - // Extract the version from the CR name, owner-dataset-v1-0 -> owner-dataset + v1-0 - versionMatcher := regexp.MustCompile("^(.*)(?:-(v?[1-9](?:-[0-9])?))?$") - match := versionMatcher.FindStringSubmatch(obj.GetName()) - - nameParts := strings.Split(match[1], "-") - nameParts[0] = strings.ToUpper(nameParts[0]) - - // Add service version if found - if len(match) > 2 && len(match[2]) > 0 { - nameParts = append(nameParts, strings.ReplaceAll(match[2], "-", "_")) - } - - // Add inspire - if obj.Inspire() != nil { - nameParts = append(nameParts, "INSPIRE") - } - - return strings.Join(append(nameParts, string(obj.Type())), " ") -} - -func getMatchRule(url smoothoperatormodel.URL) string { - host := url.Hostname() - if strings.Contains(host, "localhost") { - return "Host(`localhost`) && Path(`" + url.Path + "`)" - } - - return "(Host(`localhost`) || Host(`" + host + "`)) && Path(`" + url.Path + "`)" -} - -func getLegendMatchRule(url smoothoperatormodel.URL) string { - host := url.Hostname() - if strings.Contains(host, "localhost") { - return "Host(`localhost`) && PathPrefix(`" + url.Path + "/legend`)" - } - - return "(Host(`localhost`) || Host(`" + host + "`)) && PathPrefix(`" + url.Path + "/legend`)" -} diff --git a/internal/controller/legendgenerator/legend-fixer.sh b/internal/controller/legendgenerator/legend-fixer.sh deleted file mode 100755 index 188bce4..0000000 --- a/internal/controller/legendgenerator/legend-fixer.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -eo pipefail -echo "creating legends for root and group layers by concatenating data layers" -input_filepath="/input/input" -remove_filepath="/input/remove" -config_filepath="/input/ogc-webservice-proxy-config.yaml" -legend_dir="/var/www/legend" -< "${input_filepath}" xargs -n 2 echo | while read -r layer style; do - export layer - # shellcheck disable=SC2016 # dollar is for yq - if ! < "${config_filepath}" yq -e 'env(layer) as $layer | .grouplayers | keys | contains([$layer])' &>/dev/null; then - continue - fi - export grouplayer="${layer}" - grouplayer_style_filepath="${legend_dir}/${grouplayer}/${style}.png" - # shellcheck disable=SC2016 # dollar is for yq - datalayers=$(< "${config_filepath}" yq 'env(grouplayer) as $foo | .grouplayers[$foo][]') - datalayer_style_filepaths=() - for datalayer in $datalayers; do - datalayer_style_filepath="${legend_dir}/${datalayer}/${style}.png" - if [[ -f "${datalayer_style_filepath}" ]]; then - datalayer_style_filepaths+=("${datalayer_style_filepath}") - fi - done - if [[ -n "${datalayer_style_filepaths[*]}" ]]; then - echo "concatenating ${grouplayer_style_filepath}" - gm convert -append "${datalayer_style_filepaths[@]}" "${grouplayer_style_filepath}" - else - echo "no data for ${grouplayer_style_filepath}" - fi -done -< "${remove_filepath}" xargs -n 2 echo | while read -r layer style; do - remove_legend_file="${legend_dir}/${layer}/${style}.png" - echo removing $remove_legend_file - rm $remove_legend_file -done -echo "done" \ No newline at end of file diff --git a/internal/controller/legendgenerator/legend_generator.go b/internal/controller/legendgenerator/legend_generator.go deleted file mode 100644 index 22935d8..0000000 --- a/internal/controller/legendgenerator/legend_generator.go +++ /dev/null @@ -1,85 +0,0 @@ -package legendgenerator - -import ( - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/constants" - "github.com/pdok/mapserver-operator/internal/controller/mapserver" - "github.com/pdok/mapserver-operator/internal/controller/types" - "github.com/pdok/mapserver-operator/internal/controller/utils" - corev1 "k8s.io/api/core/v1" -) - -func GetLegendGeneratorInitContainer(wms *pdoknlv3.WMS, images types.Images) (*corev1.Container, error) { - initContainer := corev1.Container{ - Name: constants.LegendGeneratorName, - Image: images.MapserverImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Env: []corev1.EnvVar{ - { - Name: "MAPSERVER_CONFIG_FILE", - Value: "/srv/mapserver/config/default_mapserver.conf", - }, - mapserver.GetMapfileEnvVar(wms), - }, - Command: []string{ - "bash", - "-c", - `set -eu; -exit_code=0; -cat /input/input | xargs -n 2 echo | while read layer style; do -echo Generating legend for layer: $layer, style: $style; -mkdir -p /var/www/legend/$layer; -mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; -magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); -if [[ $magic_bytes != 'PNG' ]]; then -echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; -exit_code=1; -fi; -done; -exit $exit_code; -`, - }, - VolumeMounts: []corev1.VolumeMount{ - utils.GetBaseVolumeMount(), - utils.GetDataVolumeMount(), - {Name: constants.MapserverName, MountPath: "/srv/mapserver/config/default_mapserver.conf", SubPath: "default_mapserver.conf"}, - }, - } - - if wms.Spec.Service.Mapfile != nil { - initContainer.VolumeMounts = append(initContainer.VolumeMounts, utils.GetMapfileVolumeMount()) - } - - // Adding config volumemount here to get the same order as in the old ansible operator - initContainer.VolumeMounts = append(initContainer.VolumeMounts, utils.GetConfigVolumeMount(constants.ConfigMapLegendGeneratorVolumeName)) - - return &initContainer, nil -} - -func GetLegendFixerInitContainer(images types.Images) *corev1.Container { - return &corev1.Container{ - Name: constants.LegendFixerName, - Image: images.MultitoolImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{ - "/bin/bash", - "/input/legend-fixer.sh", - }, - VolumeMounts: []corev1.VolumeMount{ - utils.GetDataVolumeMount(), - utils.GetConfigVolumeMount(constants.ConfigMapLegendGeneratorVolumeName), - }, - } -} - -func GetConfigMapData(wms *pdoknlv3.WMS) map[string]string { - data := map[string]string{ - "default_mapserver.conf": defaultMapserverConf, - } - - addLayerInput(wms, data) - if wms.Options().RewriteGroupToDataLayers { - addLegendFixerConfig(wms, data) - } - return data -} diff --git a/internal/controller/legendgenerator/legend_generator_test.go b/internal/controller/legendgenerator/legend_generator_test.go deleted file mode 100644 index 0bd4d0c..0000000 --- a/internal/controller/legendgenerator/legend_generator_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package legendgenerator - -import ( - "os" - "testing" - - "github.com/google/go-cmp/cmp" - - "github.com/pdok/mapserver-operator/api/v2beta1" - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/stretchr/testify/assert" - "sigs.k8s.io/yaml" -) - -func test(t *testing.T, name string) { - input, err := os.ReadFile("test_data/input/" + name + ".yaml") - assert.NoError(t, err) - var v2wms v2beta1.WMS - err = yaml.Unmarshal(input, &v2wms) - assert.NoError(t, err) - var wms pdoknlv3.WMS - err = v2wms.ToV3(&wms) - assert.NoError(t, err) - - expected, err := os.ReadFile("test_data/expected/" + name + ".yaml") - assert.NoError(t, err) - - expectedMap := make(map[string]string) - err = yaml.Unmarshal(expected, &expectedMap) - assert.NoError(t, err) - - diff := cmp.Diff(expectedMap, GetConfigMapData(&wms)) - assert.Equal(t, diff, "", "diff in %s, -want +got: %s", name, diff) -} - -func TestGetConfigMapDataNoLegendFix(t *testing.T) { - test(t, "no-legend-fix") -} - -func TestGetConfigMapDataLegendFix(t *testing.T) { - test(t, "legend-fix") -} diff --git a/internal/controller/legendgenerator/mapper.go b/internal/controller/legendgenerator/mapper.go deleted file mode 100644 index bf2a312..0000000 --- a/internal/controller/legendgenerator/mapper.go +++ /dev/null @@ -1,140 +0,0 @@ -package legendgenerator - -import ( - "fmt" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - - _ "embed" - "strings" - - "sigs.k8s.io/yaml" -) - -// TODO Reuse default_mapserver.conf from static_files? -const ( - defaultMapserverConf = `CONFIG - ENV - MS_MAP_NO_PATH "true" - END -END -` -) - -//go:embed legend-fixer.sh -var legendFixerScript string - -type LegendReference struct { - Layer string `yaml:"layer" json:"layer"` - Style string `yaml:"style" json:"style"` -} - -type OgcWebserviceProxyConfig struct { - GroupLayers map[string][]string `yaml:"grouplayers" json:"grouplayers"` -} - -func addLayerInput(wms *pdoknlv3.WMS, data map[string]string) { - legendReferences := make([]LegendReference, 0) - - processLayer(&wms.Spec.Service.Layer, &legendReferences) - - sb := strings.Builder{} - for _, reference := range legendReferences { - sb.WriteString(fmt.Sprintf("\"%s\" \"%s\"\n", reference.Layer, reference.Style)) - } - - data["input"] = sb.String() - - // TODO - current config is hard to read and process down the line - // We should alter the legend generation script so it can handle yaml input and pass the data as follows - // referencesYaml, err := yaml.Marshal(legendReferences) - // if err == nil { - // data["input2"] = string(referencesYaml) - // } -} - -func processLayer(layer *pdoknlv3.Layer, legendReferences *[]LegendReference) { - if !layer.Visible { - return - } - for _, style := range layer.Styles { - if style.Legend == nil { - *legendReferences = append(*legendReferences, LegendReference{ - Layer: *layer.Name, - Style: style.Name, - }) - } - } - - if layer.Layers != nil { - for _, innerLayer := range layer.Layers { - processLayer(&innerLayer, legendReferences) - } - } -} - -func addLegendFixerConfig(wms *pdoknlv3.WMS, data map[string]string) { - data["legend-fixer.sh"] = legendFixerScript - - topLayer := wms.Spec.Service.Layer - - legendReferences := make([]LegendReference, 0) - topLevelStyleNames := make(map[string]bool) - - for _, style := range topLayer.Styles { - topLevelStyleNames[style.Name] = true - } - - if topLayer.Layers != nil { - // These layers are called 'middle layers' in the old operator - for _, layer := range wms.Spec.Service.Layer.Layers { - for _, style := range layer.Styles { - if topLevelStyleNames[style.Name] && style.Legend == nil { - legendReferences = append(legendReferences, LegendReference{ - Layer: *layer.Name, - Style: style.Name, - }) - } - } - } - } - - sb := strings.Builder{} - for _, reference := range legendReferences { - sb.WriteString(fmt.Sprintf("\"%s\" \"%s\"\n", reference.Layer, reference.Style)) - } - - data["remove"] = sb.String() - - groupLayers := make(map[string][]string) - - if topLayer.IsGroupLayer() && topLayer.Name != nil { - layerName := topLayer.Name - targetArray := make([]string, 0) - getAllNestedNonGroupLayerNames(&topLayer, &targetArray) - groupLayers[*layerName] = targetArray - - for _, subLayer := range topLayer.Layers { - if subLayer.IsGroupLayer() { - layerName = subLayer.Name - targetArray = make([]string, 0) - getAllNestedNonGroupLayerNames(&subLayer, &targetArray) - groupLayers[*layerName] = targetArray - } - } - } - - ogcWebServiceProxyConfig := OgcWebserviceProxyConfig{GroupLayers: groupLayers} - proxyConfigData, _ := yaml.Marshal(ogcWebServiceProxyConfig) - data["ogc-webservice-proxy-config.yaml"] = string(proxyConfigData) -} - -func getAllNestedNonGroupLayerNames(layer *pdoknlv3.Layer, target *[]string) { - for _, subLayer := range layer.Layers { - if subLayer.IsGroupLayer() { - getAllNestedNonGroupLayerNames(&subLayer, target) - } else { - *target = append(*target, *subLayer.Name) - } - } -} diff --git a/internal/controller/legendgenerator/test_data/expected/legend-fix.yaml b/internal/controller/legendgenerator/test_data/expected/legend-fix.yaml deleted file mode 100644 index a2149e5..0000000 --- a/internal/controller/legendgenerator/test_data/expected/legend-fix.yaml +++ /dev/null @@ -1,121 +0,0 @@ -default_mapserver.conf: | - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - END -input: | - "Kadastralekaart" "standaard" - "Kadastralekaart" "kwaliteit" - "Kadastralekaart" "print" - "Bebouwing" "standaard:bebouwing" - "Bebouwing" "kwaliteit:bebouwing" - "Bebouwing" "print:bebouwing" - "Bebouwingvlak" "standaard" - "Bebouwingvlak" "kwaliteit" - "Bebouwingvlak" "print" - "Bebouwingvlak" "standaard:bebouwing" - "Bebouwingvlak" "kwaliteit:bebouwing" - "Bebouwingvlak" "print:bebouwing" - "Nummeraanduidingreeks" "standaard" - "Nummeraanduidingreeks" "kwaliteit" - "Nummeraanduidingreeks" "print" - "Nummeraanduidingreeks" "standaard:bebouwing" - "Nummeraanduidingreeks" "kwaliteit:bebouwing" - "Nummeraanduidingreeks" "print:bebouwing" - "OpenbareRuimteNaam" "standaard" - "OpenbareRuimteNaam" "kwaliteit" - "OpenbareRuimteNaam" "print" - "OpenbareRuimteNaam" "standaard:openbareruimtenaam" - "OpenbareRuimteNaam" "kwaliteit:openbareruimtenaam" - "OpenbareRuimteNaam" "print:openbareruimtenaam" - "Perceel" "standaard:perceel" - "Perceel" "kwaliteit:perceel" - "Perceel" "print:perceel" - "Perceelvlak" "standaard" - "Perceelvlak" "kwaliteit" - "Perceelvlak" "print" - "Perceelvlak" "standaard:perceel" - "Perceelvlak" "kwaliteit:perceel" - "Perceelvlak" "print:perceel" - "Label" "standaard" - "Label" "standaard:perceel" - "Label" "kwaliteit" - "Label" "kwaliteit:perceel" - "Label" "print" - "Label" "print:perceel" - "Bijpijling" "standaard" - "Bijpijling" "kwaliteit" - "Bijpijling" "print" - "Bijpijling" "standaard:perceel" - "Bijpijling" "kwaliteit:perceel" - "Bijpijling" "print:perceel" - "KadastraleGrens" "standaard" - "KadastraleGrens" "kwaliteit" - "KadastraleGrens" "print" - "KadastraleGrens" "standaard:kadastralegrens" - "KadastraleGrens" "kwaliteit:kadastralegrens" - "KadastraleGrens" "print:kadastralegrens" -legend-fixer.sh: |- - #!/usr/bin/env bash - set -eo pipefail - echo "creating legends for root and group layers by concatenating data layers" - input_filepath="/input/input" - remove_filepath="/input/remove" - config_filepath="/input/ogc-webservice-proxy-config.yaml" - legend_dir="/var/www/legend" - < "${input_filepath}" xargs -n 2 echo | while read -r layer style; do - export layer - # shellcheck disable=SC2016 # dollar is for yq - if ! < "${config_filepath}" yq -e 'env(layer) as $layer | .grouplayers | keys | contains([$layer])' &>/dev/null; then - continue - fi - export grouplayer="${layer}" - grouplayer_style_filepath="${legend_dir}/${grouplayer}/${style}.png" - # shellcheck disable=SC2016 # dollar is for yq - datalayers=$(< "${config_filepath}" yq 'env(grouplayer) as $foo | .grouplayers[$foo][]') - datalayer_style_filepaths=() - for datalayer in $datalayers; do - datalayer_style_filepath="${legend_dir}/${datalayer}/${style}.png" - if [[ -f "${datalayer_style_filepath}" ]]; then - datalayer_style_filepaths+=("${datalayer_style_filepath}") - fi - done - if [[ -n "${datalayer_style_filepaths[*]}" ]]; then - echo "concatenating ${grouplayer_style_filepath}" - gm convert -append "${datalayer_style_filepaths[@]}" "${grouplayer_style_filepath}" - else - echo "no data for ${grouplayer_style_filepath}" - fi - done - < "${remove_filepath}" xargs -n 2 echo | while read -r layer style; do - remove_legend_file="${legend_dir}/${layer}/${style}.png" - echo removing $remove_legend_file - rm $remove_legend_file - done - echo "done" -ogc-webservice-proxy-config.yaml: | - grouplayers: - Bebouwing: - - Bebouwingvlak - - Nummeraanduidingreeks - Kadastralekaart: - - Bebouwingvlak - - Nummeraanduidingreeks - - OpenbareRuimteNaam - - Perceelvlak - - Label - - Bijpijling - - KadastraleGrens - Perceel: - - Perceelvlak - - Label - - Bijpijling -remove: | - "OpenbareRuimteNaam" "standaard" - "OpenbareRuimteNaam" "kwaliteit" - "OpenbareRuimteNaam" "print" - "KadastraleGrens" "standaard" - "KadastraleGrens" "kwaliteit" - "KadastraleGrens" "print" - \ No newline at end of file diff --git a/internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml b/internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml deleted file mode 100644 index 298b271..0000000 --- a/internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml +++ /dev/null @@ -1,14 +0,0 @@ -default_mapserver.conf: | - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - END -input: | - "wegvakken" "wegvakken" - "hectopunten" "hectopunten" -#input2: | -# - layer: wegvakken -# style: wegvakken -# - layer: hectopunten -# style: hectopunten diff --git a/internal/controller/legendgenerator/test_data/input/legend-fix.yaml b/internal/controller/legendgenerator/test_data/input/legend-fix.yaml deleted file mode 100644 index ba779ca..0000000 --- a/internal/controller/legendgenerator/test_data/input/legend-fix.yaml +++ /dev/null @@ -1,527 +0,0 @@ -apiVersion: pdok.nl/v2beta1 -kind: WMS -metadata: - name: kadaster-kadastralekaart - labels: - dataset-owner: kadaster - dataset: kadastralekaart - service-version: v5_0 - service-type: wms -spec: - general: - datasetOwner: kadaster - dataset: kadastralekaart - serviceVersion: v5_0 - kubernetes: - healthCheck: - querystring: language=dut&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=193882.0336615453998,470528.1693874415942,193922.4213813782844,470564.250484353397&CRS=EPSG:28992&WIDTH=769&HEIGHT=687&LAYERS=OpenbareRuimteNaam,Bebouwing,Perceel,KadastraleGrens&FORMAT=image/png&DPI=96&MAP_RESOLUTION=96&FORMAT_OPTIONS=dpi:96&TRANSPARENT=TRUE - mimetype: image/png - resources: - limits: - memory: "100M" - ephemeralStorage: "200M" - requests: - cpu: "500" - memory: "100M" - ephemeralStorage: "100M" - options: - automaticCasing: true - disableWebserviceProxy: false - includeIngress: true - validateRequests: true - rewriteGroupToDataLayers: true - service: - inspire: false - title: Kadastrale Kaart (WMS) - abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen informatie kan vastleggen en presenteren. - keywords: - - Kadaster - - Kadastrale percelen - - Kadastrale grens - - Kadastrale kaart - - Bebouwing - - Nummeraanduidingreeks - - Openbare ruimte naam - - Perceel - - Grens - - Kwaliteit - - Kwaliteitslabels - - HVD - - Geospatiale data - metadataIdentifier: 97cf6a64-9cfc-4ce6-9741-2db44fd27fca - authority: - name: kadaster - url: https://www.kadaster.nl - dataEPSG: EPSG:28992 - resolution: 91 - defResolution: 91 - extent: "-25000 250000 280000 860000" - maxSize: 10000 - stylingAssets: - configMapRefs: - - name: ${INCLUDES} - blobKeys: - - ${BLOBS_RESOURCES_BUCKET}/fonts/liberation-sans.ttf - - ${BLOBS_RESOURCES_BUCKET}/fonts/liberation-sans-italic.ttf - layers: - - name: Kadastralekaart - title: KadastraleKaartv5 - abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen informatie kan vastleggen en presenteren. - maxScale: 6001 - keywords: - - Kadaster - - Kadastrale percelen - - Kadastrale grens - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - - name: Bebouwing - visible: true - group: Kadastralekaart - title: Bebouwing - abstract: De laag Bebouwing is een selectie op panden van de BGT. - keywords: - - Bebouwing - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 6001 - styles: - - name: standaard:bebouwing - title: Standaardvisualisatie Bebouwing - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - - name: kwaliteit:bebouwing - title: Kwaliteitsvisualisatie Bebouwing - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - - name: print:bebouwing - title: Printvisualisatie Bebouwing - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - - name: Bebouwingvlak - visible: true - group: Bebouwing - title: Bebouwingvlak - abstract: De laag Bebouwing is een selectie op panden van de BGT. - keywords: - - Bebouwing - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 6001 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: bebouwing.style - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: bebouwing_kwaliteit.style - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: bebouwing_print.style - - name: standaard:bebouwing - title: Standaardvisualisatie Bebouwing - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: bebouwing.group.style - - name: kwaliteit:bebouwing - title: Kwaliteitsvisualisatie Bebouwing - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: bebouwing_kwaliteit.group.style - - name: print:bebouwing - title: Printvisualisatie Bebouwing - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: bebouwing_print.group.style - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/pand.gpkg - table: pand - geometryType: Polygon - columns: - - object_begin_tijd - - lv_publicatiedatum - - relatieve_hoogteligging - - in_onderzoek - - tijdstip_registratie - - identificatie_namespace - - identificatie_lokaal_id - - bronhouder - - bgt_status - - plus_status - - identificatie_bag_pnd - aliases: - lv_publicatiedatum: LV-publicatiedatum - identificatie_lokaal_id: identificatieLokaalID - identificatie_bag_pnd: identificatieBAGPND - bgt_status: bgt-status - plus_status: plus-status - - name: Nummeraanduidingreeks - visible: true - group: Bebouwing - title: Nummeraanduidingreeks - abstract: De laag Bebouwing is een selectie op panden van de BGT. - keywords: - - Nummeraanduidingreeks - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 2001 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaarvisualisatie van de nummeraanduidingreeks. - visualization: nummeraanduidingreeks.style - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: nummeraanduidingreeks_kwaliteit.style - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: nummeraanduidingreeks_print.style - - name: standaard:bebouwing - title: Standaardvisualisatie Bebouwing - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: nummeraanduidingreeks.group.style - - name: kwaliteit:bebouwing - title: Kwaliteitsvisualisatie Bebouwing - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: nummeraanduidingreeks_kwaliteit.group.style - - name: print:bebouwing - title: Printvisualisatie Bebouwing - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: nummeraanduidingreeks_print.group.style - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/pand_nummeraanduiding.gpkg - table: pand_nummeraanduiding - geometryType: Point - columns: - - bebouwing_id - - hoek - - tekst - - bag_vbo_laagste_huisnummer - - bag_vbo_hoogste_huisnummer - - hoek - aliases: - bebouwing_id: bebouwingID - bag_vbo_laagste_huisnummer: identificatie_BAGVBOLaagsteHuisnummer - bag_vbo_hoogste_huisnummer: identificatie_BAGVBOHoogsteHuisnummer - - name: OpenbareRuimteNaam - visible: true - group: Kadastralekaart - title: OpenbareRuimteNaam - abstract: De laag Openbareruimtenaam is een selectie op de openbare ruimte labels van de BGT met een bgt-status "bestaand" die een classificatie (openbareruimtetype) Weg en Water hebben. - keywords: - - Openbare ruimte naam - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 2001 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: openbareruimtenaam.style - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: openbareruimtenaam_kwaliteit.style - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: openbareruimtenaam_print.style - - name: standaard:openbareruimtenaam - title: Standaardvisualisatie OpenbareRuimteNaam - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: openbareruimtenaam.group.style - - name: kwaliteit:openbareruimtenaam - title: Kwaliteitsvisualisatie OpenbareRuimteNaam - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: openbareruimtenaam_kwaliteit.group.style - - name: print:openbareruimtenaam - title: Printvisualisatie OpenbareRuimteNaam - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: openbareruimtenaam_print.group.style - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/openbareruimtelabel.gpkg - table: openbareruimtelabel - geometryType: Point - columns: - - object_begin_tijd - - lv_publicatiedatum - - relatieve_hoogteligging - - in_onderzoek - - tijdstip_registratie - - identificatie_namespace - - identificatie_lokaal_id - - bronhouder - - bgt_status - - plus_status - - identificatie_bag_opr - - tekst - - hoek - - openbare_ruimte_type - aliases: - lv_publicatiedatum: LV-publicatiedatum - identificatie_lokaal_id: identificatieLokaalID - identificatie_bag_opr: identificatieBAGOPR - bgt_status: bgt-status - plus_status: plus-status - - name: Perceel - visible: true - group: Kadastralekaart - title: Perceel - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. - keywords: - - Perceel - - Kadastrale percelen - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 6001 - styles: - - name: standaard:perceel - title: Standaardvisualisatie Perceel - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - - name: print:perceel - title: Printvisualisatie Perceel - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - - name: Perceelvlak - visible: true - group: Perceel - title: Perceelvlak - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. - keywords: - - Kadastrale percelen - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 6001 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: perceelvlak.style - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: perceelvlak_kwaliteit.style - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: perceelvlak_print.style - - name: standaard:perceel - title: Standaardvisualisatie Perceel - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: perceelvlak.group.style - - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: perceelvlak_kwaliteit.group.style - - name: print:perceel - title: Printvisualisatie Perceel - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: perceelvlak_print.group.style - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/perceel.gpkg - table: perceel - geometryType: Polygon - columns: - - identificatie_namespace - - identificatie_lokaal_id - - begin_geldigheid - - tijdstip_registratie - - volgnummer - - status_historie_code - - status_historie_waarde - - kadastrale_gemeente_code - - kadastrale_gemeente_waarde - - sectie - - akr_kadastrale_gemeente_code_code - - akr_kadastrale_gemeente_code_waarde - - kadastrale_grootte_waarde - - soort_grootte_code - - soort_grootte_waarde - - perceelnummer - - perceelnummer_rotatie - - perceelnummer_verschuiving_delta_x - - perceelnummer_verschuiving_delta_y - - perceelnummer_plaatscoordinaat_x - - perceelnummer_plaatscoordinaat_y - aliases: - identificatie_lokaal_id: identificatieLokaalID - akr_kadastrale_gemeente_code_code: AKRKadastraleGemeenteCodeCode - akr_kadastrale_gemeente_code_waarde: AKRKadastraleGemeenteCodeWaarde - - name: Label - visible: true - group: Perceel - title: Label - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. - keywords: - - Kadastrale percelen - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 6001 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaarvisualisatie van het label. - visualization: label.style - - name: standaard:perceel - title: Standaardvisualisatie Perceel - abstract: Standaarvisualisatie van het label. - visualization: label.group.style - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: label_kwaliteit.style - - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: label_kwaliteit.group.style - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: label_print.style - - name: print:perceel - title: Printvisualisatie Perceel - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: label_print.group.style - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/perceel_label.gpkg - table: perceel_label - geometryType: Point - columns: - - perceel_id - - perceelnummer - - rotatie - - verschuiving_delta_x - - verschuiving_delta_y - aliases: - perceel_id: perceelID - - name: Bijpijling - visible: true - group: Perceel - title: Bijpijling - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. - keywords: - - Kadastrale percelen - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 6001 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: bijpijling.style - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: bijpijling_kwaliteit.style - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: bijpijling_print.style - - name: standaard:perceel - title: Standaardvisualisatie Perceel - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: bijpijling.group.style - - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: bijpijling_kwaliteit.group.style - - name: print:perceel - title: Printvisualisatie Perceel - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: bijpijling_print.group.style - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/perceel_bijpijling.gpkg - table: perceel_bijpijling - geometryType: LineString - columns: - - perceel_id - aliases: - perceel_id: perceelID - - name: KadastraleGrens - visible: true - group: Kadastralekaart - title: KadastraleGrens - abstract: Een Kadastrale Grens is de weergave van een grens op de kadastrale kaart die door de dienst van het Kadaster tussen percelen (voorlopig) vastgesteld wordt, op basis van inlichtingen van belanghebbenden en met gebruikmaking van de aan de kadastrale kaart ten grondslag liggende bescheiden die in elk geval de landmeetkundige gegevens bevatten van hetgeen op die kaart wordt weergegeven. - keywords: - - Grens - - Kadastrale grenzen - datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - minScale: 50 - maxScale: 6001 - styles: - - name: standaard - title: Standaardvisualisatie - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: kadastralegrens.style - - name: kwaliteit - title: Kwaliteitsvisualisatie - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: kadastralegrens_kwaliteit.style - - name: print - title: Printvisualisatie - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: kadastralegrens_print.style - - name: standaard:kadastralegrens - title: Standaardvisualisatie KadastraleGrens - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). - visualization: kadastralegrens.group.style - - name: kwaliteit:kadastralegrens - title: Kwaliteitsvisualisatie KadastraleGrens - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). - visualization: kadastralegrens_kwaliteit.group.style - - name: print:kadastralegrens - title: Printvisualisatie KadastraleGrens - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - visualization: kadastralegrens_print.group.style - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/kadastrale_grens.gpkg - table: kadastrale_grens - geometryType: LineString - columns: - - begin_geldigheid - - tijdstip_registratie - - volgnummer - - status_historie_code - - status_historie_waarde - - identificatie_namespace - - identificatie_lokaal_id - - type_grens_code - - type_grens_waarde - - classificatie_kwaliteit_code - - classificatie_kwaliteit_waarde - - perceel_links_identificatie_namespace - - perceel_links_identificatie_lokaal_id - - perceel_rechts_identificatie_namespace - - perceel_rechts_identificatie_lokaal_id - aliases: - identificatie_lokaal_id: identificatieLokaalID - perceel_links_identificatie_lokaal_id: perceelLinksIdentificatieLokaalID - perceel_rechts_identificatie_lokaal_id: perceelRechtsIdentificatieLokaalID - classificatie_kwaliteit_code: ClassificatieKwaliteitCode - classificatie_kwaliteit_waarde: ClassificatieKwaliteitWaarde diff --git a/internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml b/internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml deleted file mode 100644 index 7cacc96..0000000 --- a/internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml +++ /dev/null @@ -1,193 +0,0 @@ -apiVersion: pdok.nl/v2beta1 -kind: WMS -metadata: - name: rws-nwbwegen-v1-0 - labels: - dataset-owner: rws - dataset: nwbwegen - service-version: v1_0 - service-type: wms - annotations: - lifecycle-phase: prod - service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb -spec: - general: - datasetOwner: rws - dataset: nwbwegen - serviceVersion: v1_0 - kubernetes: - healthCheck: - boundingbox: 135134.89,457152.55,135416.03,457187.82 - resources: - limits: - ephemeralStorage: 1535Mi - memory: 4G - requests: - cpu: 2000m - ephemeralStorage: 1535Mi - memory: 4G - options: - automaticCasing: true - disableWebserviceProxy: false - includeIngress: true - validateRequests: true - service: - title: NWB - Wegen WMS - abstract: - Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. - Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen - Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. - Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, - provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien - van een straatnaam of nummer. - authority: - name: rws - url: https://www.rijkswaterstaat.nl - dataEPSG: EPSG:28992 - extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961 - inspire: true - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - - Hectometerpunten - - HVD - - Mobiliteit - stylingAssets: - configMapRefs: - - name: includes - keys: - - nwb_wegen_hectopunten.symbol - - hectopunten.style - - wegvakken.style - blobKeys: - - resources/fonts/liberation-sans.ttf - layers: - - abstract: - Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) - en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, - routenummer, wegbeheerder, huisnummers, enz. weer. - data: - gpkg: - columns: - - objectid - - wvk_id - - wvk_begdat - - jte_id_beg - - jte_id_end - - wegbehsrt - - wegnummer - - wegdeelltr - - hecto_lttr - - bst_code - - rpe_code - - admrichtng - - rijrichtng - - stt_naam - - stt_bron - - wpsnaam - - gme_id - - gme_naam - - hnrstrlnks - - hnrstrrhts - - e_hnr_lnks - - e_hnr_rhts - - l_hnr_lnks - - l_hnr_rhts - - begafstand - - endafstand - - beginkm - - eindkm - - pos_tv_wol - - wegbehcode - - wegbehnaam - - distrcode - - distrnaam - - dienstcode - - dienstnaam - - wegtype - - wgtype_oms - - routeltr - - routenr - - routeltr2 - - routenr2 - - routeltr3 - - routenr3 - - routeltr4 - - routenr4 - - wegnr_aw - - wegnr_hmp - - geobron_id - - geobron_nm - - bronjaar - - openlr - - bag_orl - - frc - - fow - - alt_naam - - alt_nr - - rel_hoogte - - st_lengthshape - geometryType: MultiLineString - blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg - table: wegvakken - datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - maxScale: 50000.0 - minScale: 1.0 - name: wegvakken - sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - styles: - - name: wegvakken - title: NWB - Wegvakken - visualization: wegvakken.style - title: Wegvakken - visible: true - - abstract: - Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) - en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, - zijde en hectoletter weer. - data: - gpkg: - columns: - - objectid - - hectomtrng - - afstand - - wvk_id - - wvk_begdat - - zijde - - hecto_lttr - geometryType: MultiPoint - blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg - table: hectopunten - datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Hectometerpunten - maxScale: 50000.0 - minScale: 1.0 - name: hectopunten - sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - styles: - - name: hectopunten - title: NWB - Hectopunten - visualization: hectopunten.style - title: Hectopunten - visible: true - metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8 diff --git a/internal/controller/mapfilegenerator/mapfile_generator.go b/internal/controller/mapfilegenerator/mapfile_generator.go deleted file mode 100644 index 694a62e..0000000 --- a/internal/controller/mapfilegenerator/mapfile_generator.go +++ /dev/null @@ -1,84 +0,0 @@ -package mapfilegenerator - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - "github.com/pdok/mapserver-operator/internal/controller/types" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/utils" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - corev1 "k8s.io/api/core/v1" -) - -func GetMapfileGeneratorInitContainer[O pdoknlv3.WMSWFS](obj O, images types.Images) (*corev1.Container, error) { - initContainer := corev1.Container{ - Name: constants.MapfileGeneratorName, - Image: images.MapfileGeneratorImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{"generate-mapfile"}, - Args: []string{ - "--not-include", - strings.ToLower(string(obj.Type())), - "/input/input.json", - "/srv/data/config/mapfile", - }, - VolumeMounts: []corev1.VolumeMount{ - utils.GetBaseVolumeMount(), - utils.GetConfigVolumeMount(constants.ConfigMapMapfileGeneratorVolumeName), - }, - } - - if obj.Type() == pdoknlv3.ServiceTypeWMS { - stylingFilesVolMount := corev1.VolumeMount{Name: constants.ConfigMapStylingFilesVolumeName, MountPath: "/styling", ReadOnly: true} - initContainer.VolumeMounts = append(initContainer.VolumeMounts, stylingFilesVolMount) - } - - return &initContainer, nil -} - -func GetConfig[W pdoknlv3.WMSWFS](webservice W, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { - switch any(webservice).(type) { - case *pdoknlv3.WFS: - if WFS, ok := any(webservice).(*pdoknlv3.WFS); ok { - return createConfigForWFS(WFS, ownerInfo) - } - case *pdoknlv3.WMS: - if WMS, ok := any(webservice).(*pdoknlv3.WMS); ok { - return createConfigForWMS(WMS, ownerInfo) - } - default: - return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) - } - return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) -} - -func createConfigForWFS(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { - input, err := MapWFSToMapfileGeneratorInput(wfs, ownerInfo) - if err != nil { - return "", err - } - - jsonConfig, err := json.MarshalIndent(input, "", " ") - if err != nil { - return "", err - } - return string(jsonConfig), nil -} - -func createConfigForWMS(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { - input, err := MapWMSToMapfileGeneratorInput(wms, ownerInfo) - if err != nil { - return "", err - } - - jsonConfig, err := json.MarshalIndent(input, "", " ") - if err != nil { - return "", err - } - return string(jsonConfig), nil -} diff --git a/internal/controller/mapfilegenerator/mapfile_generator_test.go b/internal/controller/mapfilegenerator/mapfile_generator_test.go deleted file mode 100644 index 3d6726e..0000000 --- a/internal/controller/mapfilegenerator/mapfile_generator_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package mapfilegenerator - -import ( - "encoding/json" - "os" - "testing" - - "k8s.io/apimachinery/pkg/util/validation/field" - - "github.com/google/go-cmp/cmp" - - "github.com/stretchr/testify/assert" - "sigs.k8s.io/yaml" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -func TestGetConfigForWFS(t *testing.T) { - pdoknlv3.SetHost("https://service.pdok.nl") - ownerInfo := &smoothoperatorv1.OwnerInfo{ - Spec: smoothoperatorv1.OwnerInfoSpec{ - NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), - }, - } - - input, err := os.ReadFile("test_data/input/wfs.yaml") - assert.NoError(t, err) - inputWfs := pdoknlv3.WFS{} - err = yaml.Unmarshal(input, &inputWfs) - assert.NoError(t, err) - warnings := []string{} - allErrs := field.ErrorList{} - pdoknlv3.ValidateWFS(&inputWfs, &warnings, &allErrs) - - inputStruct, err := MapWFSToMapfileGeneratorInput(&inputWfs, ownerInfo) - assert.NoError(t, err) - expected, err := readExpectedWFS("wfs.json") - assert.NoError(t, err) - - diff := cmp.Diff(expected, inputStruct) - assert.Equal(t, diff, "", "%s", diff) -} - -func TestGetConfigForWMSWithNoGroupLayers(t *testing.T) { - testWMS(t, "wms_groupless") -} - -func TestGetConfigForWMSWithGroupLayers(t *testing.T) { - testWMS(t, "wms_group") -} - -func TestGetConfigForWMSWithGroupLayersAndTopGroupLayer(t *testing.T) { - testWMS(t, "wms_group_and_toplayer") -} - -func TestGetConfigForTifWMS(t *testing.T) { - testWMS(t, "wms_tif") -} - -func TestGetConfigForPostgisWMS(t *testing.T) { - testWMS(t, "wms_postgis") -} - -func testWMS(t *testing.T, filenameWithoutExt string) { - pdoknlv3.SetHost("https://service.pdok.nl") - ownerInfo := &smoothoperatorv1.OwnerInfo{ - Spec: smoothoperatorv1.OwnerInfoSpec{ - NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), - }, - } - - input, err := os.ReadFile("test_data/input/" + filenameWithoutExt + ".yaml") - assert.NoError(t, err) - var wms pdoknlv3.WMS - err = yaml.Unmarshal(input, &wms) - assert.NoError(t, err) - inputStruct, err := MapWMSToMapfileGeneratorInput(&wms, ownerInfo) - assert.NoError(t, err) - expected, err := readExpectedWMS(filenameWithoutExt + ".json") - assert.NoError(t, err) - - diff := cmp.Diff(expected, inputStruct) - assert.Equal(t, diff == "", true, "%s", diff) -} - -func readExpectedWMS(filename string) (WMSInput, error) { - bytes, err := os.ReadFile("test_data/expected/" + filename) - if err != nil { - return WMSInput{}, err - } - - expected := WMSInput{} - err = json.Unmarshal(bytes, &expected) - - return expected, err -} - -func readExpectedWFS(filename string) (WFSInput, error) { - bytes, err := os.ReadFile("test_data/expected/" + filename) - if err != nil { - return WFSInput{}, err - } - - expected := WFSInput{} - err = json.Unmarshal(bytes, &expected) - - return expected, err -} diff --git a/internal/controller/mapfilegenerator/mapper.go b/internal/controller/mapfilegenerator/mapper.go deleted file mode 100644 index ce3121e..0000000 --- a/internal/controller/mapfilegenerator/mapper.go +++ /dev/null @@ -1,325 +0,0 @@ -package mapfilegenerator - -import ( - "slices" - "strconv" - "strings" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/mapperutils" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -const ( - defaultMaxFeatures = 1000 - tifPath = "/srv/data/tif" - geopackagePath = "/srv/data/gpkg" - defaultExtent = "-25000 250000 280000 860000" -) - -var mapserverDebugLevel = 0 - -func SetDebugLevel(level int) { - if level < 0 || level > 5 { - panic("level must be between 0 and 5") - } - - mapserverDebugLevel = level -} - -func MapWFSToMapfileGeneratorInput(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (WFSInput, error) { - var metadataID string - if wfs.Spec.Service.Inspire != nil { - metadataID = wfs.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier - } - - extent := defaultExtent - if wfs.Spec.Service.Bbox != nil { - extent = wfs.Spec.Service.Bbox.DefaultCRS.ToExtent() - } - - input := WFSInput{ - BaseServiceInput: BaseServiceInput{ - Title: wfs.Spec.Service.Title, - Abstract: wfs.Spec.Service.Abstract, - Keywords: strings.Join(wfs.Spec.Service.KeywordsIncludingInspireKeyword(), ","), - OnlineResource: wfs.URL().Scheme + "://" + wfs.URL().Host, - Path: wfs.URL().Path, - MetadataID: metadataID, - Extent: extent, - NamespacePrefix: wfs.Spec.Service.Prefix, - NamespaceURI: mapperutils.GetNamespaceURI(wfs.Spec.Service.Prefix, ownerInfo), - AutomaticCasing: wfs.Options().AutomaticCasing, - DataEPSG: wfs.Spec.Service.DefaultCrs, - EPSGList: append([]string{wfs.Spec.Service.DefaultCrs}, wfs.Spec.Service.OtherCrs...), - DebugLevel: mapserverDebugLevel, - AccessConstraints: wfs.Spec.Service.AccessConstraints.String(), - }, - MaxFeatures: strconv.Itoa(smoothoperatorutils.PointerVal(wfs.Spec.Service.CountDefault, defaultMaxFeatures)), - Layers: getWFSLayers(wfs.Spec.Service), - } - - return input, nil -} - -func getWFSLayers(service pdoknlv3.WFSService) (layers []WFSLayer) { - for _, featureType := range service.FeatureTypes { - layer := WFSLayer{ - BaseLayer: BaseLayer{ - Name: featureType.Name, - Title: featureType.Title, - Abstract: featureType.Abstract, - Keywords: strings.Join(featureType.Keywords, ","), - Extent: getWFSExtent(featureType, service), - MetadataID: featureType.DatasetMetadataURL.CSW.MetadataIdentifier, - Columns: getColumns(featureType.Data), - TableName: featureType.Data.GetTableName(), - GeometryType: featureType.Data.GetGeometryType(), - GeopackagePath: getGeopackagePath(featureType.Data.Gpkg), - }, - } - if featureType.Data.Postgis != nil { - layer.Postgis = smoothoperatorutils.Pointer(true) - } - - layers = append(layers, layer) - } - - return -} - -func getWFSExtent(featureType pdoknlv3.FeatureType, service pdoknlv3.WFSService) string { - if featureType.Bbox != nil && featureType.Bbox.DefaultCRS != nil { - return featureType.Bbox.DefaultCRS.ToExtent() - } - if service.Bbox != nil { - return service.Bbox.DefaultCRS.ToExtent() - } - return defaultExtent -} - -func getWMSExtent(serviceLayer pdoknlv3.Layer, serviceExtent string) string { - if len(serviceLayer.BoundingBoxes) > 0 { - return serviceLayer.BoundingBoxes[0].ToExtent() - } - if serviceExtent != "" { - return serviceExtent - } - return defaultExtent -} - -func getColumns(data pdoknlv3.BaseData) []Column { - columns := []Column{{Name: "fuuid"}} - if data.GetColumns() != nil { - for _, column := range *data.GetColumns() { - columns = append(columns, Column{Name: column.Name, Alias: column.Alias}) - } - } else { - return nil - } - return columns -} - -func getGeopackagePath(gpkg *pdoknlv3.Gpkg) *string { - if gpkg == nil { - return nil - } - index := strings.LastIndex(gpkg.BlobKey, "/") + 1 - blobName := gpkg.BlobKey[index:] - return smoothoperatorutils.Pointer(geopackagePath + "/" + blobName) -} - -func MapWMSToMapfileGeneratorInput(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (WMSInput, error) { - service := wms.Spec.Service - - authority := wms.GetAuthority() - authorityURL := "" - datasetOwner := "" - if authority != nil { - authorityURL = authority.URL - datasetOwner = authority.Name - } - - box := service.GetBoundingBox() - extent := box.ToExtent() - - maxSize := "4000" - if service.MaxSize != nil { - maxSize = strconv.Itoa(int(*service.MaxSize)) - } - - metadataID := "" - if service.Inspire != nil { - metadataID = service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier - } - - var fonts *string - - if service.StylingAssets != nil { - writeFonts := mapperutils.AnyMatch(service.StylingAssets.BlobKeys, func(s string) bool { - return strings.HasSuffix(s, ".ttf") - }) - - if writeFonts { - fonts = smoothoperatorutils.Pointer("/srv/data/config/fonts") - } - } - - result := WMSInput{ - BaseServiceInput: BaseServiceInput{ - Title: service.Title, - Abstract: service.Abstract, - Keywords: strings.Join(wms.Spec.Service.KeywordsIncludingInspireKeyword(), ","), - Extent: extent, - NamespacePrefix: wms.Spec.Service.Prefix, - NamespaceURI: mapperutils.GetNamespaceURI(wms.Spec.Service.Prefix, ownerInfo), - OnlineResource: wms.URL().Scheme + "://" + wms.URL().Host, - Path: wms.URL().Path, - MetadataID: metadataID, - DatasetOwner: &datasetOwner, - AuthorityURL: &authorityURL, - AutomaticCasing: wms.Options().AutomaticCasing, - DataEPSG: service.DataEPSG, - AccessConstraints: service.AccessConstraints.String(), - }, - Layers: []WMSLayer{}, - GroupLayers: []GroupLayer{}, - Symbols: getSymbols(wms), - Fonts: fonts, - OutputFormatJpg: "jpg", - OutputFormatPng: "png", - Templates: constants.HTMLTemplatesPath, - MaxSize: maxSize, - } - - if wms.Spec.Service.Layer.Name != nil { - result.TopLevelName = *wms.Spec.Service.Layer.Name - } - - if wms.Spec.Service.Resolution != nil { - result.Resolution = strconv.Itoa(int(*wms.Spec.Service.Resolution)) - } - if wms.Spec.Service.DefResolution != nil { - result.DefResolution = strconv.Itoa(int(*wms.Spec.Service.DefResolution)) - } - - mapLayers(wms, extent, &result) - - return result, nil -} - -func mapLayers(wms *pdoknlv3.WMS, extent string, result *WMSInput) { - epsgs := []string{} - - annotatedLayers := wms.Spec.Service.GetAnnotatedLayers() - for _, annotatedLayer := range annotatedLayers { - - for _, bbox := range annotatedLayer.BoundingBoxes { - if !slices.Contains(epsgs, bbox.CRS) { - epsgs = append(epsgs, bbox.CRS) - } - } - - if annotatedLayer.IsDataLayer { - layer := getWMSLayer(annotatedLayer.Layer, extent, wms) - result.Layers = append(result.Layers, layer) - } else if annotatedLayer.IsGroupLayer && !annotatedLayer.IsTopLayer { - groupLayer := GroupLayer{ - Name: *annotatedLayer.Layer.Name, - Title: smoothoperatorutils.PointerVal(annotatedLayer.Layer.Title, ""), - Abstract: smoothoperatorutils.PointerVal(annotatedLayer.Layer.Abstract, ""), - StyleName: "", - StyleTitle: "", - } - if len(annotatedLayer.Layer.Styles) > 0 { - groupLayer.StyleName = annotatedLayer.Layer.Styles[0].Name - groupLayer.StyleTitle = smoothoperatorutils.PointerVal(annotatedLayer.Layer.Styles[0].Title, "") - } - result.GroupLayers = append(result.GroupLayers, groupLayer) - } - } - - if !slices.Contains(epsgs, "CRS:84") { - epsgs = append(epsgs, "CRS:84") - } - - result.EPSGList = epsgs -} - -func getWMSLayer(serviceLayer pdoknlv3.Layer, serviceExtent string, wms *pdoknlv3.WMS) WMSLayer { - groupName := "" - parent := wms.Spec.Service.GetParentLayer(serviceLayer) - // If the layer falls directly under the toplayer, the groupname is omitted - if !parent.IsTopLayer() && parent.IsGroupLayer() && parent.Name != nil && parent.Visible { - groupName = *parent.Name - } - - var columns []Column - if serviceLayer.Data != nil { - columns = getColumns(serviceLayer.Data.BaseData) - } - - var tableName *string - if serviceLayer.Data != nil { - tableName = serviceLayer.Data.GetTableName() - } - - metadataID := "" - if serviceLayer.DatasetMetadataURL != nil && serviceLayer.DatasetMetadataURL.CSW != nil { - metadataID = serviceLayer.DatasetMetadataURL.CSW.MetadataIdentifier - } - - result := WMSLayer{ - BaseLayer: BaseLayer{ - Name: *serviceLayer.Name, - Title: smoothoperatorutils.PointerVal(serviceLayer.Title, ""), - Abstract: smoothoperatorutils.PointerVal(serviceLayer.Abstract, ""), - Keywords: strings.Join(serviceLayer.Keywords, ","), - Extent: getWMSExtent(serviceLayer, serviceExtent), - MetadataID: metadataID, - Columns: columns, - GeometryType: nil, - GeopackagePath: nil, - TableName: tableName, - Postgis: nil, - MinScale: serviceLayer.MinScaleDenominator, - MaxScale: serviceLayer.MaxScaleDenominator, - LabelNoClip: serviceLayer.LabelNoClip, - }, - GroupName: groupName, - Styles: []Style{}, - Offsite: "", - } - - for _, style := range serviceLayer.Styles { - stylePath := "/styling/" + smoothoperatorutils.PointerVal(style.Visualization, "") - result.Styles = append(result.Styles, Style{ - Path: stylePath, - Title: smoothoperatorutils.PointerVal(style.Title, ""), - }) - } - - if serviceLayer.Data != nil { - SetDataFields(wms, &result, *serviceLayer.Data) - } - - return result -} - -func getSymbols(wms *pdoknlv3.WMS) []string { - result := make([]string, 0) - service := wms.Spec.Service - if service.StylingAssets != nil { - for _, ref := range service.StylingAssets.ConfigMapRefs { - for _, key := range ref.Keys { - if strings.HasSuffix(key, ".symbol") { - result = append(result, "/styling/"+key) - } - } - } - } - return result -} diff --git a/internal/controller/mapfilegenerator/test_data/expected/wfs.json b/internal/controller/mapfilegenerator/test_data/expected/wfs.json deleted file mode 100644 index 0855366..0000000 --- a/internal/controller/mapfilegenerator/test_data/expected/wfs.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "service_title": "some Service title", - "service_abstract": "some \"Service\" abstract", - "service_keywords": "service-keyword-1,service-keyword-2,infoFeatureAccessService", - "service_extent": "0.0 2.0 1.0 3.0", - "service_wfs_maxfeatures": "1000", - "service_namespace_prefix": "prefix", - "service_namespace_uri": "http://prefix.geonovum.nl", - "service_onlineresource": "https://service.pdok.nl", - "service_path": "/datasetOwner/dataset/theme/wfs/v1_0", - "service_metadata_id": "metameta-meta-meta-meta-metametameta", - "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "automatic_casing": true, - "data_epsg": "EPSG:28992", - "service_debug_level": 0, - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326" - ], - "layers": [ - { - "name": "featuretype-1-name", - "title": "featuretype-1-title", - "abstract": "feature \"1\" abstract", - "keywords": "featuretype-1-keyword-1,featuretype-1-keyword-2", - "layer_extent": "0.0 2.0 1.0 3.0", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "featuretype-1-column-1" - }, - { - "name": "featuretype-1-column-2" - } - ], - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/file-1.gpkg", - "tablename": "featuretype-1" - }, - { - "name": "featuretype-2-name", - "title": "featuretype-2-title", - "abstract": "feature \"2\" abstract", - "keywords": "featuretype-2-keyword-1,featuretype-2-keyword-2", - "layer_extent": "0.0 2.0 1.0 3.0", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "featuretype-2-column-1", - "alias": "alias_featuretype-2-column-1" - }, - { - "name": "featuretype-2-column-2" - } - ], - "geometry_type": "MultiLine", - "tablename": "featuretype-2", - "postgis": true - } - ] -} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_group.json b/internal/controller/mapfilegenerator/test_data/expected/wms_group.json deleted file mode 100644 index 57a00d4..0000000 --- a/internal/controller/mapfilegenerator/test_data/expected/wms_group.json +++ /dev/null @@ -1,468 +0,0 @@ -{ - "authority_url": "https://www.hetwaterschapshuis.nl/", - "automatic_casing": true, - "data_epsg": "EPSG:28992", - "dataset_owner": "hwh", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "group_layers": [ - { - "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", - "name": "HY.PhysicalWaters.ManMadeObject", - "style_name": "HY.PhysicalWaters.ManMadeObject.Default", - "style_title": "Man-made objects default style", - "title": "Man-made Object" - } - ], - "layers": [ - { - "abstract": "Watercourse", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "condition" - }, - { - "name": "delineationknown" - }, - { - "name": "endlifespanversion" - }, - { - "name": "length" - }, - { - "name": "level" - }, - { - "name": "localid" - }, - { - "name": "localtype" - }, - { - "name": "name" - }, - { - "name": "namespace" - }, - { - "name": "origin" - }, - { - "name": "persistence" - }, - { - "name": "streamorder" - }, - { - "name": "tidal" - }, - { - "name": "widthrange" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "MultiLineString", - "gpkg_path": "/srv/data/gpkg/Waterbody.gpkg", - "keywords": "Hydroobject,Waterbody,Watercourse,River,Stream,Lake,Reservoir", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.Waterbodies", - "styles": [ - { - "path": "/styling/watercourse.style", - "title": "Water bodies default style" - } - ], - "tablename": "watercourse", - "title": "Waterbody" - }, - { - "abstract": "Drainage basin", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "area" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "endlifespanversion" - }, - { - "name": "localid" - }, - { - "name": "name" - }, - { - "name": "namespace" - }, - { - "name": "order" - }, - { - "name": "orderscheme" - }, - { - "name": "origin" - }, - { - "name": "outlet" - }, - { - "name": "scope" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "MultiPolygon", - "gpkg_path": "/srv/data/gpkg/Catchment.gpkg", - "keywords": "AfvoergebiedAanvoergebied,Catchment,Basin,Catchment Area,Drainage basin", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.Catchments", - "styles": [ - { - "path": "/styling/drainagebasin.style", - "title": "Drainage Basin default style" - } - ], - "tablename": "drainagebasin", - "title": "Catchment" - }, - { - "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "condition" - }, - { - "name": "endlifespanversion" - }, - { - "name": "localid" - }, - { - "name": "name" - }, - { - "name": "namespace" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", - "group_name": "HY.PhysicalWaters.ManMadeObject", - "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.ManMadeObject.lock", - "styles": [ - { - "path": "/styling/lock.style", - "title": "Lock" - } - ], - "tablename": "lock", - "title": "Man-made Object" - }, - { - "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "condition" - }, - { - "name": "endlifespanversion" - }, - { - "name": "localid" - }, - { - "name": "name" - }, - { - "name": "namespace" - }, - { - "name": "type" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", - "group_name": "HY.PhysicalWaters.ManMadeObject", - "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.ManMadeObject.crossingpoint", - "styles": [ - { - "path": "/styling/crossingpoint.style", - "title": "Crossing Point" - } - ], - "tablename": "crossingpoint", - "title": "Man-made Object" - }, - { - "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "condition" - }, - { - "name": "endlifespanversion" - }, - { - "name": "localid" - }, - { - "name": "name" - }, - { - "name": "namespace" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", - "group_name": "HY.PhysicalWaters.ManMadeObject", - "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.ManMadeObject.sluice", - "styles": [ - { - "path": "/styling/sluice.style", - "title": "Sluice" - } - ], - "tablename": "sluice", - "title": "Man-made Object" - }, - { - "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "condition" - }, - { - "name": "endlifespanversion" - }, - { - "name": "localid" - }, - { - "name": "name" - }, - { - "name": "namespace" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "MultiLineString", - "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", - "group_name": "HY.PhysicalWaters.ManMadeObject", - "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.ManMadeObject.embankment", - "styles": [ - { - "path": "/styling/embankment.style", - "title": "Embankment" - } - ], - "tablename": "embankment", - "title": "Man-made Object" - }, - { - "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "condition" - }, - { - "name": "endlifespanversion" - }, - { - "name": "localid" - }, - { - "name": "name" - }, - { - "name": "namespace" - }, - { - "name": "type" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "MultiLineString", - "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", - "group_name": "HY.PhysicalWaters.ManMadeObject", - "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.ManMadeObject.crossingline", - "styles": [ - { - "path": "/styling/crossingline.style", - "title": "Crossing Line" - } - ], - "tablename": "crossingline", - "title": "Man-made Object" - }, - { - "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "gml_id" - }, - { - "name": "beginlifespanversion" - }, - { - "name": "condition" - }, - { - "name": "endlifespanversion" - }, - { - "name": "localid" - }, - { - "name": "name" - }, - { - "name": "namespace" - } - ], - "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", - "group_name": "HY.PhysicalWaters.ManMadeObject", - "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "HY.PhysicalWaters.ManMadeObject.damorweir", - "styles": [ - { - "path": "/styling/damorweir.style", - "title": "Dam or Weir" - } - ], - "tablename": "damorweir", - "title": "Man-made Object" - } - ], - "maxSize": "4000", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "service_abstract": "Deze dataset is gebaseerd op (niet geharmoniseerde) data van alle waterschappen in Nederland conform INSPIRE. De dataset bevat de volgende INSPIRE objecten: CrossingLine, CrossingPoint, DamOrWeir, DrainageBasin, Embankment, Lock, Sluice, Watercourse.", - "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "service_keywords": "Aquaduct,Brug,Duikersifonhevel,Stuw,Vastedam,AfvoerAanvoergebied,Waterkering,Sluis,HydroObject,HVD,Aardobservatie en milieu,infoMapAccessService", - "service_metadata_id": "871a58f8-c9f1-41a4-be37-0f059e0f886f", - "service_namespace_prefix": "hydrografie", - "service_namespace_uri": "http://hydrografie.geonovum.nl", - "service_onlineresource": "https://service.pdok.nl", - "service_path": "/hwh/hydrografie/wms/v2_0", - "service_title": "Waterschappen Hydrografie (INSPIRE geharmoniseerd) WMS", - "symbols": [ - "/styling/bridge.symbol", - "/styling/x.symbol" - ], - "templates": "/srv/data/config/templates" -} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json b/internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json deleted file mode 100644 index 5a6f8fe..0000000 --- a/internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json +++ /dev/null @@ -1,616 +0,0 @@ -{ - "authority_url": "https://www.kadaster.nl", - "automatic_casing": true, - "data_epsg": "EPSG:28992", - "dataset_owner": "kadaster", - "defresolution": "91", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "fonts": "/srv/data/config/fonts", - "group_layers": [ - { - "abstract": "De laag Bebouwing is een selectie op panden van de BGT.", - "name": "Bebouwing", - "style_name": "standaard:bebouwing", - "style_title": "Standaardvisualisatie Bebouwing", - "title": "Bebouwing" - }, - { - "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", - "name": "Perceel", - "style_name": "standaard:perceel", - "style_title": "Standaardvisualisatie Perceel", - "title": "Perceel" - } - ], - "layers": [ - { - "abstract": "De laag Bebouwing is een selectie op panden van de BGT.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "object_begin_tijd" - }, - { - "alias": "LV-publicatiedatum", - "name": "lv_publicatiedatum" - }, - { - "name": "relatieve_hoogteligging" - }, - { - "name": "in_onderzoek" - }, - { - "name": "tijdstip_registratie" - }, - { - "name": "identificatie_namespace" - }, - { - "alias": "identificatieLokaalID", - "name": "identificatie_lokaal_id" - }, - { - "name": "bronhouder" - }, - { - "alias": "bgt-status", - "name": "bgt_status" - }, - { - "alias": "plus-status", - "name": "plus_status" - }, - { - "alias": "identificatieBAGPND", - "name": "identificatie_bag_pnd" - } - ], - "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", - "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", - "geometry_type": "Polygon", - "gpkg_path": "/srv/data/gpkg/pand.gpkg", - "group_name": "Bebouwing", - "keywords": "Bebouwing", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "6001", - "minscale": "50", - "name": "Bebouwingvlak", - "styles": [ - { - "path": "/styling/bebouwing.style", - "title": "Standaardvisualisatie" - }, - { - "path": "/styling/bebouwing_kwaliteit.style", - "title": "Kwaliteitsvisualisatie" - }, - { - "path": "/styling/bebouwing_print.style", - "title": "Printvisualisatie" - }, - { - "path": "/styling/bebouwing.group.style", - "title": "Standaardvisualisatie Bebouwing" - }, - { - "path": "/styling/bebouwing_kwaliteit.group.style", - "title": "Kwaliteitsvisualisatie Bebouwing" - }, - { - "path": "/styling/bebouwing_print.group.style", - "title": "Printvisualisatie Bebouwing" - } - ], - "tablename": "pand", - "title": "Bebouwingvlak" - }, - { - "abstract": "De laag Bebouwing is een selectie op panden van de BGT.", - "columns": [ - { - "name": "fuuid" - }, - { - "alias": "bebouwingID", - "name": "bebouwing_id" - }, - { - "name": "hoek" - }, - { - "name": "tekst" - }, - { - "alias": "identificatie_BAGVBOLaagsteHuisnummer", - "name": "bag_vbo_laagste_huisnummer" - }, - { - "alias": "identificatie_BAGVBOHoogsteHuisnummer", - "name": "bag_vbo_hoogste_huisnummer" - }, - { - "name": "hoek" - } - ], - "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", - "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/pand_nummeraanduiding.gpkg", - "group_name": "Bebouwing", - "keywords": "Nummeraanduidingreeks", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "2001", - "minscale": "50", - "name": "Nummeraanduidingreeks", - "styles": [ - { - "path": "/styling/nummeraanduidingreeks.style", - "title": "Standaardvisualisatie" - }, - { - "path": "/styling/nummeraanduidingreeks_kwaliteit.style", - "title": "Kwaliteitsvisualisatie" - }, - { - "path": "/styling/nummeraanduidingreeks_print.style", - "title": "Printvisualisatie" - }, - { - "path": "/styling/nummeraanduidingreeks.group.style", - "title": "Standaardvisualisatie Bebouwing" - }, - { - "path": "/styling/nummeraanduidingreeks_kwaliteit.group.style", - "title": "Kwaliteitsvisualisatie Bebouwing" - }, - { - "path": "/styling/nummeraanduidingreeks_print.group.style", - "title": "Printvisualisatie Bebouwing" - } - ], - "tablename": "pand_nummeraanduiding", - "title": "Nummeraanduidingreeks" - }, - { - "abstract": "De laag Openbareruimtenaam is een selectie op de openbare ruimte labels van de BGT met een bgt-status \"bestaand\" die een classificatie (openbareruimtetype) Weg en Water hebben.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "object_begin_tijd" - }, - { - "alias": "LV-publicatiedatum", - "name": "lv_publicatiedatum" - }, - { - "name": "relatieve_hoogteligging" - }, - { - "name": "in_onderzoek" - }, - { - "name": "tijdstip_registratie" - }, - { - "name": "identificatie_namespace" - }, - { - "alias": "identificatieLokaalID", - "name": "identificatie_lokaal_id" - }, - { - "name": "bronhouder" - }, - { - "alias": "bgt-status", - "name": "bgt_status" - }, - { - "alias": "plus-status", - "name": "plus_status" - }, - { - "alias": "identificatieBAGOPR", - "name": "identificatie_bag_opr" - }, - { - "name": "tekst" - }, - { - "name": "hoek" - }, - { - "name": "openbare_ruimte_type" - } - ], - "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", - "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/openbareruimtelabel.gpkg", - "keywords": "Openbare ruimte naam", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "2001", - "minscale": "50", - "name": "OpenbareRuimteNaam", - "styles": [ - { - "path": "/styling/openbareruimtenaam.style", - "title": "Standaardvisualisatie" - }, - { - "path": "/styling/openbareruimtenaam_kwaliteit.style", - "title": "Kwaliteitsvisualisatie" - }, - { - "path": "/styling/openbareruimtenaam_print.style", - "title": "Printvisualisatie" - }, - { - "path": "/styling/openbareruimtenaam.group.style", - "title": "Standaardvisualisatie OpenbareRuimteNaam" - }, - { - "path": "/styling/openbareruimtenaam_kwaliteit.group.style", - "title": "Kwaliteitsvisualisatie OpenbareRuimteNaam" - }, - { - "path": "/styling/openbareruimtenaam_print.group.style", - "title": "Printvisualisatie OpenbareRuimteNaam" - } - ], - "tablename": "openbareruimtelabel", - "title": "OpenbareRuimteNaam" - }, - { - "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "identificatie_namespace" - }, - { - "alias": "identificatieLokaalID", - "name": "identificatie_lokaal_id" - }, - { - "name": "begin_geldigheid" - }, - { - "name": "tijdstip_registratie" - }, - { - "name": "volgnummer" - }, - { - "name": "status_historie_code" - }, - { - "name": "status_historie_waarde" - }, - { - "name": "kadastrale_gemeente_code" - }, - { - "name": "kadastrale_gemeente_waarde" - }, - { - "name": "sectie" - }, - { - "alias": "AKRKadastraleGemeenteCodeCode", - "name": "akr_kadastrale_gemeente_code_code" - }, - { - "alias": "AKRKadastraleGemeenteCodeWaarde", - "name": "akr_kadastrale_gemeente_code_waarde" - }, - { - "name": "kadastrale_grootte_waarde" - }, - { - "name": "soort_grootte_code" - }, - { - "name": "soort_grootte_waarde" - }, - { - "name": "perceelnummer" - }, - { - "name": "perceelnummer_rotatie" - }, - { - "name": "perceelnummer_verschuiving_delta_x" - }, - { - "name": "perceelnummer_verschuiving_delta_y" - }, - { - "name": "perceelnummer_plaatscoordinaat_x" - }, - { - "name": "perceelnummer_plaatscoordinaat_y" - } - ], - "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", - "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", - "geometry_type": "Polygon", - "gpkg_path": "/srv/data/gpkg/perceel.gpkg", - "group_name": "Perceel", - "keywords": "Kadastrale percelen", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "6001", - "minscale": "50", - "name": "Perceelvlak", - "styles": [ - { - "path": "/styling/perceelvlak.style", - "title": "Standaardvisualisatie" - }, - { - "path": "/styling/perceelvlak_kwaliteit.style", - "title": "Kwaliteitsvisualisatie" - }, - { - "path": "/styling/perceelvlak_print.style", - "title": "Printvisualisatie" - }, - { - "path": "/styling/perceelvlak.group.style", - "title": "Standaardvisualisatie Perceel" - }, - { - "path": "/styling/perceelvlak_kwaliteit.group.style", - "title": "Kwaliteitsvisualisatie Perceel" - }, - { - "path": "/styling/perceelvlak_print.group.style", - "title": "Printvisualisatie Perceel" - } - ], - "tablename": "perceel", - "title": "Perceelvlak" - }, - { - "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", - "columns": [ - { - "name": "fuuid" - }, - { - "alias": "perceelID", - "name": "perceel_id" - }, - { - "name": "perceelnummer" - }, - { - "name": "rotatie" - }, - { - "name": "verschuiving_delta_x" - }, - { - "name": "verschuiving_delta_y" - } - ], - "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", - "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/perceel_label.gpkg", - "group_name": "Perceel", - "keywords": "Kadastrale percelen", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "6001", - "minscale": "50", - "name": "Label", - "styles": [ - { - "path": "/styling/label.style", - "title": "Standaardvisualisatie" - }, - { - "path": "/styling/label.group.style", - "title": "Standaardvisualisatie Perceel" - }, - { - "path": "/styling/label_kwaliteit.style", - "title": "Kwaliteitsvisualisatie" - }, - { - "path": "/styling/label_kwaliteit.group.style", - "title": "Kwaliteitsvisualisatie Perceel" - }, - { - "path": "/styling/label_print.style", - "title": "Printvisualisatie" - }, - { - "path": "/styling/label_print.group.style", - "title": "Printvisualisatie Perceel" - } - ], - "tablename": "perceel_label", - "title": "Label" - }, - { - "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", - "columns": [ - { - "name": "fuuid" - }, - { - "alias": "perceelID", - "name": "perceel_id" - } - ], - "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", - "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", - "geometry_type": "LineString", - "gpkg_path": "/srv/data/gpkg/perceel_bijpijling.gpkg", - "group_name": "Perceel", - "keywords": "Kadastrale percelen", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "6001", - "minscale": "50", - "name": "Bijpijling", - "styles": [ - { - "path": "/styling/bijpijling.style", - "title": "Standaardvisualisatie" - }, - { - "path": "/styling/bijpijling_kwaliteit.style", - "title": "Kwaliteitsvisualisatie" - }, - { - "path": "/styling/bijpijling_print.style", - "title": "Printvisualisatie" - }, - { - "path": "/styling/bijpijling.group.style", - "title": "Standaardvisualisatie Perceel" - }, - { - "path": "/styling/bijpijling_kwaliteit.group.style", - "title": "Kwaliteitsvisualisatie Perceel" - }, - { - "path": "/styling/bijpijling_print.group.style", - "title": "Printvisualisatie Perceel" - } - ], - "tablename": "perceel_bijpijling", - "title": "Bijpijling" - }, - { - "abstract": "Een Kadastrale Grens is de weergave van een grens op de kadastrale kaart die door de dienst van het Kadaster tussen percelen (voorlopig) vastgesteld wordt, op basis van inlichtingen van belanghebbenden en met gebruikmaking van de aan de kadastrale kaart ten grondslag liggende bescheiden die in elk geval de landmeetkundige gegevens bevatten van hetgeen op die kaart wordt weergegeven.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "begin_geldigheid" - }, - { - "name": "tijdstip_registratie" - }, - { - "name": "volgnummer" - }, - { - "name": "status_historie_code" - }, - { - "name": "status_historie_waarde" - }, - { - "name": "identificatie_namespace" - }, - { - "alias": "identificatieLokaalID", - "name": "identificatie_lokaal_id" - }, - { - "name": "type_grens_code" - }, - { - "name": "type_grens_waarde" - }, - { - "alias": "ClassificatieKwaliteitCode", - "name": "classificatie_kwaliteit_code" - }, - { - "alias": "ClassificatieKwaliteitWaarde", - "name": "classificatie_kwaliteit_waarde" - }, - { - "name": "perceel_links_identificatie_namespace" - }, - { - "alias": "perceelLinksIdentificatieLokaalID", - "name": "perceel_links_identificatie_lokaal_id" - }, - { - "name": "perceel_rechts_identificatie_namespace" - }, - { - "alias": "perceelRechtsIdentificatieLokaalID", - "name": "perceel_rechts_identificatie_lokaal_id" - } - ], - "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", - "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", - "geometry_type": "LineString", - "gpkg_path": "/srv/data/gpkg/kadastrale_grens.gpkg", - "keywords": "Grens,Kadastrale grenzen", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "6001", - "minscale": "50", - "name": "KadastraleGrens", - "styles": [ - { - "path": "/styling/kadastralegrens.style", - "title": "Standaardvisualisatie" - }, - { - "path": "/styling/kadastralegrens_kwaliteit.style", - "title": "Kwaliteitsvisualisatie" - }, - { - "path": "/styling/kadastralegrens_print.style", - "title": "Printvisualisatie" - }, - { - "path": "/styling/kadastralegrens.group.style", - "title": "Standaardvisualisatie KadastraleGrens" - }, - { - "path": "/styling/kadastralegrens_kwaliteit.group.style", - "title": "Kwaliteitsvisualisatie KadastraleGrens" - }, - { - "path": "/styling/kadastralegrens_print.group.style", - "title": "Printvisualisatie KadastraleGrens" - } - ], - "tablename": "kadastrale_grens", - "title": "KadastraleGrens" - } - ], - "maxSize": "10000", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "resolution": "91", - "service_abstract": "Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen informatie kan vastleggen en presenteren.", - "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-25000 250000 280000 860000", - "service_keywords": "Kadaster,Kadastrale percelen,Kadastrale grens,Kadastrale kaart,Bebouwing,Nummeraanduidingreeks,Openbare ruimte naam,Perceel,Grens,Kwaliteit,Kwaliteitslabels,HVD,Geospatiale data", - "service_metadata_id": "", - "service_namespace_prefix": "kadastralekaart", - "service_namespace_uri": "http://kadastralekaart.geonovum.nl", - "service_onlineresource": "https://service.pdok.nl", - "service_path": "/kadaster/kadastralekaart/wms/v5_0", - "service_title": "Kadastrale Kaart (WMS)", - "symbols": [], - "templates": "/srv/data/config/templates", - "top_level_name": "Kadastralekaart" -} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json b/internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json deleted file mode 100644 index 1e92343..0000000 --- a/internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json +++ /dev/null @@ -1,283 +0,0 @@ -{ - "authority_url": "https://www.rijkswaterstaat.nl", - "automatic_casing": true, - "data_epsg": "EPSG:28992", - "dataset_owner": "rws", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "fonts": "/srv/data/config/fonts", - "group_layers": [], - "layers": [ - { - "abstract": "Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, routenummer, wegbeheerder, huisnummers, enz. weer.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "objectid" - }, - { - "name": "wvk_id" - }, - { - "name": "wvk_begdat" - }, - { - "name": "jte_id_beg" - }, - { - "name": "jte_id_end" - }, - { - "name": "wegbehsrt" - }, - { - "name": "wegnummer" - }, - { - "name": "wegdeelltr" - }, - { - "name": "hecto_lttr" - }, - { - "name": "bst_code" - }, - { - "name": "rpe_code" - }, - { - "name": "admrichtng" - }, - { - "name": "rijrichtng" - }, - { - "name": "stt_naam" - }, - { - "name": "stt_bron" - }, - { - "name": "wpsnaam" - }, - { - "name": "gme_id" - }, - { - "name": "gme_naam" - }, - { - "name": "hnrstrlnks" - }, - { - "name": "hnrstrrhts" - }, - { - "name": "e_hnr_lnks" - }, - { - "name": "e_hnr_rhts" - }, - { - "name": "l_hnr_lnks" - }, - { - "name": "l_hnr_rhts" - }, - { - "name": "begafstand" - }, - { - "name": "endafstand" - }, - { - "name": "beginkm" - }, - { - "name": "eindkm" - }, - { - "name": "pos_tv_wol" - }, - { - "name": "wegbehcode" - }, - { - "name": "wegbehnaam" - }, - { - "name": "distrcode" - }, - { - "name": "distrnaam" - }, - { - "name": "dienstcode" - }, - { - "name": "dienstnaam" - }, - { - "name": "wegtype" - }, - { - "name": "wgtype_oms" - }, - { - "name": "routeltr" - }, - { - "name": "routenr" - }, - { - "name": "routeltr2" - }, - { - "name": "routenr2" - }, - { - "name": "routeltr3" - }, - { - "name": "routenr3" - }, - { - "name": "routeltr4" - }, - { - "name": "routenr4" - }, - { - "name": "wegnr_aw" - }, - { - "name": "wegnr_hmp" - }, - { - "name": "geobron_id" - }, - { - "name": "geobron_nm" - }, - { - "name": "bronjaar" - }, - { - "name": "openlr" - }, - { - "name": "bag_orl" - }, - { - "name": "frc" - }, - { - "name": "fow" - }, - { - "name": "alt_naam" - }, - { - "name": "alt_nr" - }, - { - "name": "rel_hoogte" - }, - { - "name": "st_lengthshape" - } - ], - "dataset_metadata_id": "a9b7026e-0a81-4813-93bd-ba49e6f28502", - "dataset_source_id": "8f0497f0-dbd7-4bee-b85a-5fdec484a7ff", - "geometry_type": "MultiLineString", - "gpkg_path": "/srv/data/gpkg/nwb_wegen.gpkg", - "keywords": "Vervoersnetwerken,Menselijke gezondheid en veiligheid,Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai),Nationaal,Voertuigen,Verkeer,Wegvakken", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "wegvakken", - "styles": [ - { - "path": "/styling/wegvakken.style", - "title": "NWB - Wegvakken" - } - ], - "tablename": "wegvakken", - "title": "Wegvakken" - }, - { - "abstract": "Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, zijde en hectoletter weer.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "objectid" - }, - { - "name": "hectomtrng" - }, - { - "name": "afstand" - }, - { - "name": "wvk_id" - }, - { - "name": "wvk_begdat" - }, - { - "name": "zijde" - }, - { - "name": "hecto_lttr" - } - ], - "dataset_metadata_id": "a9b7026e-0a81-4813-93bd-ba49e6f28502", - "dataset_source_id": "8f0497f0-dbd7-4bee-b85a-5fdec484a7ff", - "geometry_type": "MultiPoint", - "gpkg_path": "/srv/data/gpkg/nwb_wegen.gpkg", - "keywords": "Vervoersnetwerken,Menselijke gezondheid en veiligheid,Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai),Nationaal,Voertuigen,Verkeer,Hectometerpunten", - "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "maxscale": "50000", - "minscale": "1", - "name": "hectopunten", - "styles": [ - { - "path": "/styling/hectopunten.style", - "title": "NWB - Hectopunten" - } - ], - "tablename": "hectopunten", - "title": "Hectopunten" - } - ], - "maxSize": "4000", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "service_abstract": "Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien van een straatnaam of nummer.", - "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", - "service_keywords": "Vervoersnetwerken,Menselijke gezondheid en veiligheid,Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai),Nationaal,Voertuigen,Verkeer,Wegvakken,Hectometerpunten,HVD,Mobiliteit,infoMapAccessService", - "service_metadata_id": "f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8", - "service_namespace_prefix": "nwbwegen", - "service_namespace_uri": "http://nwbwegen.geonovum.nl", - "service_onlineresource": "https://service.pdok.nl", - "service_path": "/rws/nwbwegen/wms/v1_0", - "service_title": "NWB - Wegen WMS", - "symbols": [ - "/styling/nwb_wegen_hectopunten.symbol" - ], - "templates": "/srv/data/config/templates" -} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json b/internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json deleted file mode 100644 index a8ff834..0000000 --- a/internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "authority_url": "http://www.brt.nl", - "automatic_casing": true, - "data_epsg": "EPSG:28992", - "dataset_owner": "brt", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "group_layers": [], - "layers": [ - { - "abstract": "Alle recente BRT terugmeldingen gedaan door BRT gebruikers.", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "meldingsnummer_volledig" - }, - { - "name": "tijdstip_registratie" - }, - { - "name": "status" - }, - { - "name": "omschrijving" - }, - { - "name": "bronhoudercode" - }, - { - "name": "bronhoudernaam" - }, - { - "name": "tijdstip_statuswijziging" - }, - { - "name": "toelichting" - }, - { - "name": "objectid" - }, - { - "name": "objecttype" - }, - { - "name": "hoogte_vanaf_maaiveld" - } - ], - "dataset_metadata_id": "7a84c4de-4ec0-4202-a8d0-792fb7d39d1f", - "dataset_source_id": "07c7d650-cdb1-11dd-ad8b-0800200c9a60", - "geometry_type": "Point", - "keywords": "brtterugmeldingen", - "layer_extent": "-7000 289000 300000 629000", - "name": "brtterugmeldingen", - "postgis": true, - "styles": [ - { - "path": "/styling/terugmeldingen.style", - "title": "Terugmeldingen" - } - ], - "tablename": "brtterugmeldingen.brtterugmeldingen_v1", - "title": "BRT Terugmeldingen" - } - ], - "maxSize": "4000", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "service_abstract": "De BRT terugmeldingenservice bevat alle recente meldingen op BRT objecten waar twijfel over de juistheid bestaat. Zowel terugmeldingen op de TOP10 als meldingen die gemaakt zijn op de gegeneraliseerde kaartproducten (TOP25, TOP50, TOP100, TOP250) worden hierin geregistreerd. Daarnaast kan je de inhoud en status van de meldingen inzien. Ook een vermoedelijke fout geconstateerd? Doe een melding op https://verbeterdekaart.kadaster.nl", - "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-7000 289000 300000 629000", - "service_keywords": "Basisregistratie Topografie,BRT,terugmeldingen,TOP10NL,TOP25,TOP50,TOP100,TOP250,in onderzoek register,verbeter de kaart,verbeterdekaart", - "service_metadata_id": "", - "service_namespace_prefix": "terugmeldingen", - "service_namespace_uri": "http://terugmeldingen.geonovum.nl", - "service_onlineresource": "https://service.pdok.nl", - "service_path": "/brt/terugmeldingen/wms/v1_0", - "service_title": "BRT Terugmeldingen WMS", - "symbols": [ - "/styling/terugmeldingen.symbol" - ], - "templates": "/srv/data/config/templates" -} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_tif.json b/internal/controller/mapfilegenerator/test_data/expected/wms_tif.json deleted file mode 100644 index 99abb1b..0000000 --- a/internal/controller/mapfilegenerator/test_data/expected/wms_tif.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "authority_url": "http://www.kadaster.nl", - "automatic_casing": false, - "data_epsg": "EPSG:28992", - "dataset_owner": "kadaster", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "group_layers": [ - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "name": "lufolabels", - "style_name": "luchtfotolabels", - "style_title": "Luchtfotolabels", - "title": "Luchtfoto labels" - } - ], - "layers": [ - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "48001", - "minscale": "24001", - "name": "luchtfotoroads_100pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/roads.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/100pixkm_luforoads.vrt", - "title": "Luchtfoto roads 100pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "24001", - "minscale": "12001", - "name": "luchtfotoroads_200pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "2.0", - "styles": [ - { - "path": "/styling/roads.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/200pixkm_luforoads.vrt", - "title": "Luchtfoto roads 200pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "12001", - "minscale": "6001", - "name": "luchtfotoroads_400pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "2.5", - "styles": [ - { - "path": "/styling/roads.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/400pixkm_luforoads.vrt", - "title": "Luchtfoto roads 400pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "6001", - "minscale": "3001", - "name": "luchtfotoroads_800pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/roads.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/800pixkm_luforoads.vrt", - "title": "Luchtfoto roads 800pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "3001", - "minscale": "1501", - "name": "luchtfotoroads_1600pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/roads.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/1600pixkm_luforoads.vrt", - "title": "Luchtfoto roads 1600pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "48001", - "minscale": "24001", - "name": "luchtfotolabels_100pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/labels.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/100pixkm_lufolabels.vrt", - "title": "Luchtfoto labels 100pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "24001", - "minscale": "12001", - "name": "luchtfotolabels_200pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/labels.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/200pixkm_lufolabels.vrt", - "title": "Luchtfoto labels 200pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "12001", - "minscale": "6001", - "name": "luchtfotolabels_400pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/labels.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/400pixkm_lufolabels.vrt", - "title": "Luchtfoto labels 400pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "6001", - "minscale": "3001", - "name": "luchtfotolabels_800pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/labels.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/800pixkm_lufolabels.vrt", - "title": "Luchtfoto labels 800pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "3001", - "minscale": "1501", - "name": "luchtfotolabels_1600pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/labels.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/1600pixkm_lufolabels.vrt", - "title": "Luchtfoto labels 1600pixkm" - }, - { - "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", - "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", - "geometry_type": "Raster", - "get_feature_info_includes_class": false, - "group_name": "lufolabels", - "keywords": "bzk,luchtfotolabels", - "layer_extent": "-25000 250000 280000 860000", - "maxscale": "1501", - "name": "luchtfotolabels_3200pixkm", - "offsite": "#978E97", - "resample": "BILINEAR", - "oversample_ratio": "1", - "styles": [ - { - "path": "/styling/labels.style", - "title": "Luchtfotolabels" - } - ], - "tif_path": "/srv/data/tif/3200pixkm_lufolabels.vrt", - "title": "Luchtfoto labels 3200pixkm" - } - ], - "maxSize": "4000", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "service_abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", - "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-25000 250000 280000 860000", - "service_keywords": "bzk,luchtfotolabels", - "service_metadata_id": "", - "service_namespace_prefix": "luchtfotolabels", - "service_namespace_uri": "http://luchtfotolabels.geonovum.nl", - "service_onlineresource": "https://service.pdok.nl", - "service_path": "/bzk/luchtfotolabels/wms/v1_0", - "service_title": "Luchtfoto Labels WMS", - "symbols": [], - "templates": "/srv/data/config/templates" -} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/input/wfs.yaml b/internal/controller/mapfilegenerator/test_data/input/wfs.yaml deleted file mode 100644 index e7ca80b..0000000 --- a/internal/controller/mapfilegenerator/test_data/input/wfs.yaml +++ /dev/null @@ -1,94 +0,0 @@ -metadata: - labels: - dataset: dataset - dataset-owner: datasetOwner - service-version: v1_0 - theme: theme -spec: - options: - automaticCasing: true - disableWebserviceProxy: false - includeIngress: false - prefetchData: false - rewriteGroupToDataLayers: false - validateChildStyleNameEqual: false - validateRequests: false - service: - abstract: some "Service" abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - bbox: - defaultCRS: - maxx: "1.0" - maxy: "3.0" - minx: "0.0" - miny: "2.0" - defaultCrs: EPSG:28992 - featureTypes: - - abstract: feature "1" abstract - bbox: - defaultCRS: - maxx: "1.0" - maxy: "3.0" - minx: "0.0" - miny: "2.0" - data: - gpkg: - blobKey: public/testme/gpkg/file-1.gpkg - columns: - - name: featuretype-1-column-1 - - name: featuretype-1-column-2 - geometryType: Point - tableName: featuretype-1 - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - featuretype-1-keyword-1 - - featuretype-1-keyword-2 - name: featuretype-1-name - title: featuretype-1-title - - abstract: feature "2" abstract - bbox: - defaultCRS: - maxx: "1.0" - maxy: "3.0" - minx: "0.0" - miny: "2.0" - data: - postgis: - columns: - - alias: alias_featuretype-2-column-1 - name: featuretype-2-column-1 - - name: featuretype-2-column-2 - geometryType: MultiLine - tableName: featuretype-2 - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - featuretype-2-keyword-1 - - featuretype-2-keyword-2 - name: featuretype-2-name - title: featuretype-2-title - inspire: - language: "" - serviceMetadataUrl: - csw: - metadataIdentifier: metameta-meta-meta-meta-metametameta - spatialDatasetIdentifier: "" - keywords: - - service-keyword-1 - - service-keyword-2 - - infoFeatureAccessService - otherCrs: - - EPSG:25831 - - EPSG:25832 - - EPSG:3034 - - EPSG:3035 - - EPSG:3857 - - EPSG:4258 - - EPSG:4326 - ownerInfoRef: "" - prefix: prefix - title: some Service title - url: "https://service.pdok.nl/datasetOwner/dataset/theme/wfs/v1_0" diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_group.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_group.yaml deleted file mode 100644 index 20e2937..0000000 --- a/internal/controller/mapfilegenerator/test_data/input/wms_group.yaml +++ /dev/null @@ -1,576 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - lifecycle-phase: prod - service-bundle-id: d30bdf62-1c12-45a5-a57d-367e642ef118 - creationTimestamp: null - labels: - dataset: hydrografie - dataset-owner: hwh - service-type: wms - service-version: v2_0 - name: hwh-hydrografie-v2-0 -spec: - options: - automaticCasing: true - disableWebserviceProxy: false - includeIngress: true - prefetchData: true - rewriteGroupToDataLayers: false - validateChildStyleNameEqual: false - validateRequests: true - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 1544Mi - requests: - ephemeral-storage: 1544Mi - service: - abstract: 'Deze dataset is gebaseerd op (niet geharmoniseerde) data van alle waterschappen - in Nederland conform INSPIRE. De dataset bevat de volgende INSPIRE objecten: - CrossingLine, CrossingPoint, DamOrWeir, DrainageBasin, Embankment, Lock, Sluice, - Watercourse.' - accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - inspire: - language: dut - serviceMetadataUrl: - csw: - metadataIdentifier: 871a58f8-c9f1-41a4-be37-0f059e0f886f - keywords: - - Aquaduct - - Brug - - Duikersifonhevel - - Stuw - - Vastedam - - AfvoerAanvoergebied - - Waterkering - - Sluis - - HydroObject - - HVD - - Aardobservatie en milieu - layer: - abstract: 'Deze dataset is gebaseerd op (niet geharmoniseerde) data van alle - waterschappen in Nederland conform INSPIRE. De dataset bevat de volgende INSPIRE - objecten: CrossingLine, CrossingPoint, DamOrWeir, DrainageBasin, Embankment, - Lock, Sluice, Watercourse.' - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - - bbox: - maxx: "795163" - maxy: "6181970" - minx: "-470271" - miny: "5562310" - crs: EPSG:25831 - - bbox: - maxx: "397827" - maxy: "6190420" - minx: "62461.6" - miny: "5565550" - crs: EPSG:25832 - - bbox: - maxx: "3220070" - maxy: "3840030" - minx: "2613360" - miny: "3509000" - crs: EPSG:3034 - - bbox: - maxx: "3644850" - maxy: "4155860" - minx: "3016760" - miny: "3812640" - crs: EPSG:3035 - - bbox: - maxx: "820873" - maxy: "7503110" - minx: "281318" - miny: "6483220" - crs: EPSG:3857 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4258 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4326 - - bbox: - maxx: "7.37403" - maxy: "55.7212" - minx: "2.52713" - miny: "50.2129" - crs: CRS:84 - keywords: - - Aquaduct - - Brug - - Duikersifonhevel - - Stuw - - Vastedam - - AfvoerAanvoergebied - - Waterkering - - Sluis - - HydroObject - - HVD - - Aardobservatie en milieu - layers: - - abstract: Watercourse - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Waterbody.gpkg - columns: - - name: gml_id - - name: beginlifespanversion - - name: condition - - name: delineationknown - - name: endlifespanversion - - name: length - - name: level - - name: localid - - name: localtype - - name: name - - name: namespace - - name: origin - - name: persistence - - name: streamorder - - name: tidal - - name: widthrange - geometryType: MultiLineString - tableName: watercourse - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Hydroobject - - Waterbody - - Watercourse - - River - - Stream - - Lake - - Reservoir - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.Waterbodies - styles: - - name: HY.PhysicalWaters.Waterbodies.Default - title: Water bodies default style - visualization: watercourse.style - title: Waterbody - visible: true - - abstract: Drainage basin - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Catchment.gpkg - columns: - - name: gml_id - - name: area - - name: beginlifespanversion - - name: endlifespanversion - - name: localid - - name: name - - name: namespace - - name: order - - name: orderscheme - - name: origin - - name: outlet - - name: scope - geometryType: MultiPolygon - tableName: drainagebasin - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - AfvoergebiedAanvoergebied - - Catchment - - Basin - - Catchment Area - - Drainage basin - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.Catchments - styles: - - name: HY.PhysicalWaters.Catchments.Default - title: Drainage Basin default style - visualization: drainagebasin.style - title: Catchment - visible: true - - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, - lock. - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Man Made - - Duikersifonhevel - - Aquaduct - - Brug - - Stuw - - Vastedam - - Waterkering - - Sluis - - Bridge - - Aquaduct - - Dam - - Weir - - Lock - - Ford - - Dyke - layers: - - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, - lock. - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg - columns: - - name: gml_id - - name: beginlifespanversion - - name: condition - - name: endlifespanversion - - name: localid - - name: name - - name: namespace - geometryType: Point - tableName: lock - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Man Made - - Duikersifonhevel - - Aquaduct - - Brug - - Stuw - - Vastedam - - Waterkering - - Sluis. - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.ManMadeObject.lock - styles: - - name: HY.PhysicalWaters.ManMadeObject.Default - title: Lock - visualization: lock.style - title: Man-made Object - visible: false - - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, - lock. - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg - columns: - - name: gml_id - - name: beginlifespanversion - - name: condition - - name: endlifespanversion - - name: localid - - name: name - - name: namespace - - name: type - geometryType: Point - tableName: crossingpoint - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Man Made - - Duikersifonhevel - - Aquaduct - - Brug - - Stuw - - Vastedam - - Waterkering - - Sluis. - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.ManMadeObject.crossingpoint - styles: - - name: HY.PhysicalWaters.ManMadeObject.Default - title: Crossing Point - visualization: crossingpoint.style - title: Man-made Object - visible: false - - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, - lock. - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg - columns: - - name: gml_id - - name: beginlifespanversion - - name: condition - - name: endlifespanversion - - name: localid - - name: name - - name: namespace - geometryType: Point - tableName: sluice - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Man Made - - Duikersifonhevel - - Aquaduct - - Brug - - Stuw - - Vastedam - - Waterkering - - Sluis. - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.ManMadeObject.sluice - styles: - - name: HY.PhysicalWaters.ManMadeObject.Default - title: Sluice - visualization: sluice.style - title: Man-made Object - visible: false - - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, - lock. - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg - columns: - - name: gml_id - - name: beginlifespanversion - - name: condition - - name: endlifespanversion - - name: localid - - name: name - - name: namespace - geometryType: MultiLineString - tableName: embankment - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Man Made - - Duikersifonhevel - - Aquaduct - - Brug - - Stuw - - Vastedam - - Waterkering - - Sluis. - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.ManMadeObject.embankment - styles: - - name: HY.PhysicalWaters.ManMadeObject.Default - title: Embankment - visualization: embankment.style - title: Man-made Object - visible: false - - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, - lock. - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg - columns: - - name: gml_id - - name: beginlifespanversion - - name: condition - - name: endlifespanversion - - name: localid - - name: name - - name: namespace - - name: type - geometryType: MultiLineString - tableName: crossingline - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Man Made - - Duikersifonhevel - - Aquaduct - - Brug - - Stuw - - Vastedam - - Waterkering - - Sluis. - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.ManMadeObject.crossingline - styles: - - name: HY.PhysicalWaters.ManMadeObject.Default - title: Crossing Line - visualization: crossingline.style - title: Man-made Object - visible: false - - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, - lock. - authority: - name: hwh - spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - url: https://www.hetwaterschapshuis.nl/ - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg - columns: - - name: gml_id - - name: beginlifespanversion - - name: condition - - name: endlifespanversion - - name: localid - - name: name - - name: namespace - geometryType: Point - tableName: damorweir - datasetMetadataUrl: - csw: - metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 - keywords: - - Man Made - - Duikersifonhevel - - Aquaduct - - Brug - - Stuw - - Vastedam - - Waterkering - - Sluis. - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.ManMadeObject.damorweir - styles: - - name: HY.PhysicalWaters.ManMadeObject.Default - title: Dam or Weir - visualization: damorweir.style - title: Man-made Object - visible: false - maxscaledenominator: "50000" - minscaledenominator: "1" - name: HY.PhysicalWaters.ManMadeObject - styles: - - name: HY.PhysicalWaters.ManMadeObject.Default - title: Man-made objects default style - title: Man-made Object - visible: true - title: Waterschappen Hydrografie (INSPIRE geharmoniseerd) WMS - visible: true - ownerInfoRef: pdok - prefix: hydrografie - stylingAssets: - blobKeys: - - resources/images/hwh/hydrografie/bridge.png - configMapRefs: - - keys: - - bridge.symbol - - x.symbol - - watercourse.style - - drainagebasin.style - - lock.style - - crossingpoint.style - - sluice.style - - embankment.style - - crossingline.style - - damorweir.style - name: includes - title: Waterschappen Hydrografie (INSPIRE geharmoniseerd) WMS - url: https://service.pdok.nl/hwh/hydrografie/wms/v2_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml deleted file mode 100644 index 8b53f3a..0000000 --- a/internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml +++ /dev/null @@ -1,783 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - pdok.nl/wms-service-metadata-uuid: 97cf6a64-9cfc-4ce6-9741-2db44fd27fca - creationTimestamp: null - labels: - dataset: kadastralekaart - dataset-owner: kadaster - service-type: wms - service-version: v5_0 - name: kadaster-kadastralekaart -spec: - healthCheck: - mimetype: image/png - querystring: language=dut&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=193882.0336615453998,470528.1693874415942,193922.4213813782844,470564.250484353397&CRS=EPSG:28992&WIDTH=769&HEIGHT=687&LAYERS=OpenbareRuimteNaam,Bebouwing,Perceel,KadastraleGrens&FORMAT=image/png&DPI=96&MAP_RESOLUTION=96&FORMAT_OPTIONS=dpi:96&TRANSPARENT=TRUE - options: - automaticCasing: true - disableWebserviceProxy: false - includeIngress: true - prefetchData: true - rewriteGroupToDataLayers: true - validateChildStyleNameEqual: false - validateRequests: true - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 104M - memory: 103M - requests: - cpu: "1001" - ephemeral-storage: 102M - memory: 101M - service: - abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert - als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak - een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen - informatie kan vastleggen en presenteren. - accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - defResolution: 91 - keywords: - - Kadaster - - Kadastrale percelen - - Kadastrale grens - - Kadastrale kaart - - Bebouwing - - Nummeraanduidingreeks - - Openbare ruimte naam - - Perceel - - Grens - - Kwaliteit - - Kwaliteitslabels - - HVD - - Geospatiale data - layer: - abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. - Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker - vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker - eigen informatie kan vastleggen en presenteren. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - - bbox: - maxx: "795163" - maxy: "6181970" - minx: "-470271" - miny: "5562310" - crs: EPSG:25831 - - bbox: - maxx: "397827" - maxy: "6190420" - minx: "62461.6" - miny: "5565550" - crs: EPSG:25832 - - bbox: - maxx: "3220070" - maxy: "3840030" - minx: "2613360" - miny: "3509000" - crs: EPSG:3034 - - bbox: - maxx: "3644850" - maxy: "4155860" - minx: "3016760" - miny: "3812640" - crs: EPSG:3035 - - bbox: - maxx: "820873" - maxy: "7503110" - minx: "281318" - miny: "6483220" - crs: EPSG:3857 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4258 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4326 - - bbox: - maxx: "7.37403" - maxy: "55.7212" - minx: "2.52713" - miny: "50.2129" - crs: CRS:84 - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Kadaster - - Kadastrale percelen - - Kadastrale grens - layers: - - abstract: De laag Bebouwing is een selectie op panden van de BGT. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Bebouwing - layers: - - abstract: De laag Bebouwing is een selectie op panden van de BGT. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/pand.gpkg - columns: - - name: object_begin_tijd - - alias: LV-publicatiedatum - name: lv_publicatiedatum - - name: relatieve_hoogteligging - - name: in_onderzoek - - name: tijdstip_registratie - - name: identificatie_namespace - - alias: identificatieLokaalID - name: identificatie_lokaal_id - - name: bronhouder - - alias: bgt-status - name: bgt_status - - alias: plus-status - name: plus_status - - alias: identificatieBAGPND - name: identificatie_bag_pnd - geometryType: Polygon - tableName: pand - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Bebouwing - maxscaledenominator: "6001" - minscaledenominator: "50" - name: Bebouwingvlak - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard - title: Standaardvisualisatie - visualization: bebouwing.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - visualization: bebouwing_kwaliteit.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - visualization: bebouwing_print.style - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:bebouwing - title: Standaardvisualisatie Bebouwing - visualization: bebouwing.group.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:bebouwing - title: Kwaliteitsvisualisatie Bebouwing - visualization: bebouwing_kwaliteit.group.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:bebouwing - title: Printvisualisatie Bebouwing - visualization: bebouwing_print.group.style - title: Bebouwingvlak - visible: true - - abstract: De laag Bebouwing is een selectie op panden van de BGT. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/pand_nummeraanduiding.gpkg - columns: - - alias: bebouwingID - name: bebouwing_id - - name: hoek - - name: tekst - - alias: identificatie_BAGVBOLaagsteHuisnummer - name: bag_vbo_laagste_huisnummer - - alias: identificatie_BAGVBOHoogsteHuisnummer - name: bag_vbo_hoogste_huisnummer - - name: hoek - geometryType: Point - tableName: pand_nummeraanduiding - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Nummeraanduidingreeks - maxscaledenominator: "2001" - minscaledenominator: "50" - name: Nummeraanduidingreeks - styles: - - abstract: Standaarvisualisatie van de nummeraanduidingreeks. - name: standaard - title: Standaardvisualisatie - visualization: nummeraanduidingreeks.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - visualization: nummeraanduidingreeks_kwaliteit.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - visualization: nummeraanduidingreeks_print.style - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:bebouwing - title: Standaardvisualisatie Bebouwing - visualization: nummeraanduidingreeks.group.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:bebouwing - title: Kwaliteitsvisualisatie Bebouwing - visualization: nummeraanduidingreeks_kwaliteit.group.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:bebouwing - title: Printvisualisatie Bebouwing - visualization: nummeraanduidingreeks_print.group.style - title: Nummeraanduidingreeks - visible: true - maxscaledenominator: "6001" - minscaledenominator: "50" - name: Bebouwing - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:bebouwing - title: Standaardvisualisatie Bebouwing - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:bebouwing - title: Kwaliteitsvisualisatie Bebouwing - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:bebouwing - title: Printvisualisatie Bebouwing - title: Bebouwing - visible: true - - abstract: De laag Openbareruimtenaam is een selectie op de openbare ruimte - labels van de BGT met een bgt-status "bestaand" die een classificatie (openbareruimtetype) - Weg en Water hebben. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/openbareruimtelabel.gpkg - columns: - - name: object_begin_tijd - - alias: LV-publicatiedatum - name: lv_publicatiedatum - - name: relatieve_hoogteligging - - name: in_onderzoek - - name: tijdstip_registratie - - name: identificatie_namespace - - alias: identificatieLokaalID - name: identificatie_lokaal_id - - name: bronhouder - - alias: bgt-status - name: bgt_status - - alias: plus-status - name: plus_status - - alias: identificatieBAGOPR - name: identificatie_bag_opr - - name: tekst - - name: hoek - - name: openbare_ruimte_type - geometryType: Point - tableName: openbareruimtelabel - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Openbare ruimte naam - maxscaledenominator: "2001" - minscaledenominator: "50" - name: OpenbareRuimteNaam - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard - title: Standaardvisualisatie - visualization: openbareruimtenaam.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - visualization: openbareruimtenaam_kwaliteit.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - visualization: openbareruimtenaam_print.style - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:openbareruimtenaam - title: Standaardvisualisatie OpenbareRuimteNaam - visualization: openbareruimtenaam.group.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:openbareruimtenaam - title: Kwaliteitsvisualisatie OpenbareRuimteNaam - visualization: openbareruimtenaam_kwaliteit.group.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:openbareruimtenaam - title: Printvisualisatie OpenbareRuimteNaam - visualization: openbareruimtenaam_print.group.style - title: OpenbareRuimteNaam - visible: true - - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft - gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een - perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal - geรฏdentificeerd is en met kadastrale grenzen begrensd is. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Perceel - - Kadastrale percelen - layers: - - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen - heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. - Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal - geรฏdentificeerd is en met kadastrale grenzen begrensd is. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/perceel.gpkg - columns: - - name: identificatie_namespace - - alias: identificatieLokaalID - name: identificatie_lokaal_id - - name: begin_geldigheid - - name: tijdstip_registratie - - name: volgnummer - - name: status_historie_code - - name: status_historie_waarde - - name: kadastrale_gemeente_code - - name: kadastrale_gemeente_waarde - - name: sectie - - alias: AKRKadastraleGemeenteCodeCode - name: akr_kadastrale_gemeente_code_code - - alias: AKRKadastraleGemeenteCodeWaarde - name: akr_kadastrale_gemeente_code_waarde - - name: kadastrale_grootte_waarde - - name: soort_grootte_code - - name: soort_grootte_waarde - - name: perceelnummer - - name: perceelnummer_rotatie - - name: perceelnummer_verschuiving_delta_x - - name: perceelnummer_verschuiving_delta_y - - name: perceelnummer_plaatscoordinaat_x - - name: perceelnummer_plaatscoordinaat_y - geometryType: Polygon - tableName: perceel - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Kadastrale percelen - maxscaledenominator: "6001" - minscaledenominator: "50" - name: Perceelvlak - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard - title: Standaardvisualisatie - visualization: perceelvlak.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - visualization: perceelvlak_kwaliteit.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - visualization: perceelvlak_print.style - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:perceel - title: Standaardvisualisatie Perceel - visualization: perceelvlak.group.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - visualization: perceelvlak_kwaliteit.group.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:perceel - title: Printvisualisatie Perceel - visualization: perceelvlak_print.group.style - title: Perceelvlak - visible: true - - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen - heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. - Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal - geรฏdentificeerd is en met kadastrale grenzen begrensd is. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/perceel_label.gpkg - columns: - - alias: perceelID - name: perceel_id - - name: perceelnummer - - name: rotatie - - name: verschuiving_delta_x - - name: verschuiving_delta_y - geometryType: Point - tableName: perceel_label - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Kadastrale percelen - maxscaledenominator: "6001" - minscaledenominator: "50" - name: Label - styles: - - abstract: Standaarvisualisatie van het label. - name: standaard - title: Standaardvisualisatie - visualization: label.style - - abstract: Standaarvisualisatie van het label. - name: standaard:perceel - title: Standaardvisualisatie Perceel - visualization: label.group.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - visualization: label_kwaliteit.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - visualization: label_kwaliteit.group.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - visualization: label_print.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:perceel - title: Printvisualisatie Perceel - visualization: label_print.group.style - title: Label - visible: true - - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen - heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. - Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal - geรฏdentificeerd is en met kadastrale grenzen begrensd is. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/perceel_bijpijling.gpkg - columns: - - alias: perceelID - name: perceel_id - geometryType: LineString - tableName: perceel_bijpijling - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Kadastrale percelen - maxscaledenominator: "6001" - minscaledenominator: "50" - name: Bijpijling - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard - title: Standaardvisualisatie - visualization: bijpijling.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - visualization: bijpijling_kwaliteit.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - visualization: bijpijling_print.style - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:perceel - title: Standaardvisualisatie Perceel - visualization: bijpijling.group.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - visualization: bijpijling_kwaliteit.group.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:perceel - title: Printvisualisatie Perceel - visualization: bijpijling_print.group.style - title: Bijpijling - visible: true - maxscaledenominator: "6001" - minscaledenominator: "50" - name: Perceel - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:perceel - title: Standaardvisualisatie Perceel - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:perceel - title: Kwaliteitsvisualisatie Perceel - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:perceel - title: Printvisualisatie Perceel - title: Perceel - visible: true - - abstract: Een Kadastrale Grens is de weergave van een grens op de kadastrale - kaart die door de dienst van het Kadaster tussen percelen (voorlopig) vastgesteld - wordt, op basis van inlichtingen van belanghebbenden en met gebruikmaking - van de aan de kadastrale kaart ten grondslag liggende bescheiden die in - elk geval de landmeetkundige gegevens bevatten van hetgeen op die kaart - wordt weergegeven. - authority: - name: kadaster - spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 - url: https://www.kadaster.nl - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/kadastrale_grens.gpkg - columns: - - name: begin_geldigheid - - name: tijdstip_registratie - - name: volgnummer - - name: status_historie_code - - name: status_historie_waarde - - name: identificatie_namespace - - alias: identificatieLokaalID - name: identificatie_lokaal_id - - name: type_grens_code - - name: type_grens_waarde - - alias: ClassificatieKwaliteitCode - name: classificatie_kwaliteit_code - - alias: ClassificatieKwaliteitWaarde - name: classificatie_kwaliteit_waarde - - name: perceel_links_identificatie_namespace - - alias: perceelLinksIdentificatieLokaalID - name: perceel_links_identificatie_lokaal_id - - name: perceel_rechts_identificatie_namespace - - alias: perceelRechtsIdentificatieLokaalID - name: perceel_rechts_identificatie_lokaal_id - geometryType: LineString - tableName: kadastrale_grens - datasetMetadataUrl: - csw: - metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 - keywords: - - Grens - - Kadastrale grenzen - maxscaledenominator: "6001" - minscaledenominator: "50" - name: KadastraleGrens - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard - title: Standaardvisualisatie - visualization: kadastralegrens.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - visualization: kadastralegrens_kwaliteit.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - visualization: kadastralegrens_print.style - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard:kadastralegrens - title: Standaardvisualisatie KadastraleGrens - visualization: kadastralegrens.group.style - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit:kadastralegrens - title: Kwaliteitsvisualisatie KadastraleGrens - visualization: kadastralegrens_kwaliteit.group.style - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print:kadastralegrens - title: Printvisualisatie KadastraleGrens - visualization: kadastralegrens_print.group.style - title: KadastraleGrens - visible: true - maxscaledenominator: "6001" - name: Kadastralekaart - styles: - - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, - voorlopig of administratief). - name: standaard - title: Standaardvisualisatie - - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse - (B, C, D of E). - name: kwaliteit - title: Kwaliteitsvisualisatie - - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. - name: print - title: Printvisualisatie - title: KadastraleKaartv5 - visible: true - maxSize: 10000 - ownerInfoRef: pdok - prefix: kadastralekaart - resolution: 91 - stylingAssets: - blobKeys: - - resources/fonts/liberation-sans.ttf - - resources/fonts/liberation-sans-italic.ttf - configMapRefs: - - keys: - - bebouwing.style - - bebouwing_kwaliteit.style - - bebouwing_print.style - - bebouwing.group.style - - bebouwing_kwaliteit.group.style - - bebouwing_print.group.style - - nummeraanduidingreeks.style - - nummeraanduidingreeks_kwaliteit.style - - nummeraanduidingreeks_print.style - - nummeraanduidingreeks.group.style - - nummeraanduidingreeks_kwaliteit.group.style - - nummeraanduidingreeks_print.group.style - - openbareruimtenaam.style - - openbareruimtenaam_kwaliteit.style - - openbareruimtenaam_print.style - - openbareruimtenaam.group.style - - openbareruimtenaam_kwaliteit.group.style - - openbareruimtenaam_print.group.style - - perceelvlak.style - - perceelvlak_kwaliteit.style - - perceelvlak_print.style - - perceelvlak.group.style - - perceelvlak_kwaliteit.group.style - - perceelvlak_print.group.style - - label.style - - label.group.style - - label_kwaliteit.style - - label_kwaliteit.group.style - - label_print.style - - label_print.group.style - - bijpijling.style - - bijpijling_kwaliteit.style - - bijpijling_print.style - - bijpijling.group.style - - bijpijling_kwaliteit.group.style - - bijpijling_print.group.style - - kadastralegrens.style - - kadastralegrens_kwaliteit.style - - kadastralegrens_print.style - - kadastralegrens.group.style - - kadastralegrens_kwaliteit.group.style - - kadastralegrens_print.group.style - name: includes - title: Kadastrale Kaart (WMS) - url: https://service.pdok.nl/kadaster/kadastralekaart/wms/v5_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml deleted file mode 100644 index 845272e..0000000 --- a/internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml +++ /dev/null @@ -1,298 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - lifecycle-phase: prod - service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb - creationTimestamp: null - labels: - dataset: nwbwegen - dataset-owner: rws - service-type: wms - service-version: v1_0 - name: rws-nwbwegen-v1-0 -spec: - healthCheck: - boundingbox: - maxx: "135416.03" - maxy: "457187.82" - minx: "135134.89" - miny: "457152.55" - options: - automaticCasing: true - disableWebserviceProxy: false - includeIngress: true - prefetchData: true - rewriteGroupToDataLayers: false - validateChildStyleNameEqual: false - validateRequests: true - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 1535Mi - memory: 4G - requests: - cpu: "2" - ephemeral-storage: 1535Mi - memory: 4G - service: - abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. - Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen - Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. - Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, - provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien - van een straatnaam of nummer. - accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - inspire: - language: dut - serviceMetadataUrl: - csw: - metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - - Hectometerpunten - - HVD - - Mobiliteit - layer: - abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal - Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in - Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders - als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover - deze zijn voorzien van een straatnaam of nummer. - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - - bbox: - maxx: "795163" - maxy: "6181970" - minx: "-470271" - miny: "5562310" - crs: EPSG:25831 - - bbox: - maxx: "397827" - maxy: "6190420" - minx: "62461.6" - miny: "5565550" - crs: EPSG:25832 - - bbox: - maxx: "3220070" - maxy: "3840030" - minx: "2613360" - miny: "3509000" - crs: EPSG:3034 - - bbox: - maxx: "3644850" - maxy: "4155860" - minx: "3016760" - miny: "3812640" - crs: EPSG:3035 - - bbox: - maxx: "820873" - maxy: "7503110" - minx: "281318" - miny: "6483220" - crs: EPSG:3857 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4258 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4326 - - bbox: - maxx: "7.37403" - maxy: "55.7212" - minx: "2.52713" - miny: "50.2129" - crs: CRS:84 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - - Hectometerpunten - - HVD - - Mobiliteit - layers: - - abstract: Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) - en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, - routenummer, wegbeheerder, huisnummers, enz. weer. - authority: - name: rws - spatialDatasetIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - url: https://www.rijkswaterstaat.nl - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg - columns: - - name: objectid - - name: wvk_id - - name: wvk_begdat - - name: jte_id_beg - - name: jte_id_end - - name: wegbehsrt - - name: wegnummer - - name: wegdeelltr - - name: hecto_lttr - - name: bst_code - - name: rpe_code - - name: admrichtng - - name: rijrichtng - - name: stt_naam - - name: stt_bron - - name: wpsnaam - - name: gme_id - - name: gme_naam - - name: hnrstrlnks - - name: hnrstrrhts - - name: e_hnr_lnks - - name: e_hnr_rhts - - name: l_hnr_lnks - - name: l_hnr_rhts - - name: begafstand - - name: endafstand - - name: beginkm - - name: eindkm - - name: pos_tv_wol - - name: wegbehcode - - name: wegbehnaam - - name: distrcode - - name: distrnaam - - name: dienstcode - - name: dienstnaam - - name: wegtype - - name: wgtype_oms - - name: routeltr - - name: routenr - - name: routeltr2 - - name: routenr2 - - name: routeltr3 - - name: routenr3 - - name: routeltr4 - - name: routenr4 - - name: wegnr_aw - - name: wegnr_hmp - - name: geobron_id - - name: geobron_nm - - name: bronjaar - - name: openlr - - name: bag_orl - - name: frc - - name: fow - - name: alt_naam - - name: alt_nr - - name: rel_hoogte - - name: st_lengthshape - geometryType: MultiLineString - tableName: wegvakken - datasetMetadataUrl: - csw: - metadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - maxscaledenominator: "50000" - minscaledenominator: "1" - name: wegvakken - styles: - - name: wegvakken - title: NWB - Wegvakken - visualization: wegvakken.style - title: Wegvakken - visible: true - - abstract: Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) - en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, - zijde en hectoletter weer. - authority: - name: rws - spatialDatasetIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - url: https://www.rijkswaterstaat.nl - boundingBoxes: - - bbox: - maxx: "308126.88473339565" - maxy: "858328.516489961" - minx: "-59188.44333693248" - miny: "304984.64144318487" - crs: EPSG:28992 - data: - gpkg: - blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg - columns: - - name: objectid - - name: hectomtrng - - name: afstand - - name: wvk_id - - name: wvk_begdat - - name: zijde - - name: hecto_lttr - geometryType: MultiPoint - tableName: hectopunten - datasetMetadataUrl: - csw: - metadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Hectometerpunten - maxscaledenominator: "50000" - minscaledenominator: "1" - name: hectopunten - styles: - - name: hectopunten - title: NWB - Hectopunten - visualization: hectopunten.style - title: Hectopunten - visible: true - title: NWB - Wegen WMS - visible: true - ownerInfoRef: pdok - prefix: nwbwegen - stylingAssets: - blobKeys: - - resources/fonts/liberation-sans.ttf - configMapRefs: - - keys: - - nwb_wegen_hectopunten.symbol - - hectopunten.style - - wegvakken.style - name: includes - title: NWB - Wegen WMS - url: https://service.pdok.nl/rws/nwbwegen/wms/v1_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml deleted file mode 100644 index 8735da6..0000000 --- a/internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml +++ /dev/null @@ -1,185 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - pdok.nl/wms-service-metadata-uuid: fa069f74-9837-4d63-b2ac-b337b5de86b1 - creationTimestamp: null - labels: - dataset: terugmeldingen - dataset-owner: brt - service-type: wms - service-version: v1_0 - name: v1-0 -spec: - options: - automaticCasing: true - disableWebserviceProxy: false - includeIngress: true - prefetchData: true - rewriteGroupToDataLayers: false - validateChildStyleNameEqual: false - validateRequests: true - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 20Mi - service: - abstract: De BRT terugmeldingenservice bevat alle recente meldingen op BRT objecten - waar twijfel over de juistheid bestaat. Zowel terugmeldingen op de TOP10 als - meldingen die gemaakt zijn op de gegeneraliseerde kaartproducten (TOP25, TOP50, - TOP100, TOP250) worden hierin geregistreerd. Daarnaast kan je de inhoud en status - van de meldingen inzien. Ook een vermoedelijke fout geconstateerd? Doe een melding - op https://verbeterdekaart.kadaster.nl - accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - keywords: - - Basisregistratie Topografie - - BRT - - terugmeldingen - - TOP10NL - - TOP25 - - TOP50 - - TOP100 - - TOP250 - - in onderzoek register - - verbeter de kaart - - verbeterdekaart - layer: - abstract: De BRT terugmeldingenservice bevat alle recente meldingen op BRT objecten - waar twijfel over de juistheid bestaat. Zowel terugmeldingen op de TOP10 als - meldingen die gemaakt zijn op de gegeneraliseerde kaartproducten (TOP25, TOP50, - TOP100, TOP250) worden hierin geregistreerd. Daarnaast kan je de inhoud en - status van de meldingen inzien. Ook een vermoedelijke fout geconstateerd? - Doe een melding op https://verbeterdekaart.kadaster.nl - boundingBoxes: - - bbox: - maxx: "300000" - maxy: "629000" - minx: "-7000" - miny: "289000" - crs: EPSG:28992 - - bbox: - maxx: "795163" - maxy: "6181970" - minx: "-470271" - miny: "5562310" - crs: EPSG:25831 - - bbox: - maxx: "397827" - maxy: "6190420" - minx: "62461.6" - miny: "5565550" - crs: EPSG:25832 - - bbox: - maxx: "3220070" - maxy: "3840030" - minx: "2613360" - miny: "3509000" - crs: EPSG:3034 - - bbox: - maxx: "3644850" - maxy: "4155860" - minx: "3016760" - miny: "3812640" - crs: EPSG:3035 - - bbox: - maxx: "820873" - maxy: "7503110" - minx: "281318" - miny: "6483220" - crs: EPSG:3857 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4258 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4326 - - bbox: - maxx: "7.37403" - maxy: "55.7212" - minx: "2.52713" - miny: "50.2129" - crs: CRS:84 - keywords: - - Basisregistratie Topografie - - BRT - - terugmeldingen - - TOP10NL - - TOP25 - - TOP50 - - TOP100 - - TOP250 - - in onderzoek register - - verbeter de kaart - - verbeterdekaart - layers: - - abstract: Alle recente BRT terugmeldingen gedaan door BRT gebruikers. - authority: - name: brt - spatialDatasetIdentifier: 07c7d650-cdb1-11dd-ad8b-0800200c9a60 - url: http://www.brt.nl - boundingBoxes: - - bbox: - maxx: "300000" - maxy: "629000" - minx: "-7000" - miny: "289000" - crs: EPSG:28992 - data: - postgis: - columns: - - name: meldingsnummer_volledig - - name: tijdstip_registratie - - name: status - - name: omschrijving - - name: bronhoudercode - - name: bronhoudernaam - - name: tijdstip_statuswijziging - - name: toelichting - - name: objectid - - name: objecttype - - name: hoogte_vanaf_maaiveld - geometryType: Point - tableName: brtterugmeldingen.brtterugmeldingen_v1 - datasetMetadataUrl: - csw: - metadataIdentifier: 7a84c4de-4ec0-4202-a8d0-792fb7d39d1f - keywords: - - brtterugmeldingen - name: brtterugmeldingen - styles: - - legend: - blobKey: ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/terugmeldingen-legend.png - name: brtterugmeldingen:terugmeldingen - title: Terugmeldingen - visualization: terugmeldingen.style - title: BRT Terugmeldingen - visible: true - title: BRT Terugmeldingen WMS - visible: true - ownerInfoRef: pdok - prefix: terugmeldingen - stylingAssets: - blobKeys: - - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/afgerond-blauw.png - - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/afgewezen-rood.png - - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/doorgestuurd-grijs.png - - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/geparkeerd-kobaltblauw.png - - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/goedgekeurd-groen.png - - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/nieuw-geel.png - - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/onderzoek-oranje.png - configMapRefs: - - keys: - - terugmeldingen.symbol - - terugmeldingen.style - name: includes - title: BRT Terugmeldingen WMS - url: https://service.pdok.nl/brt/terugmeldingen/wms/v1_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml deleted file mode 100644 index 335dd6d..0000000 --- a/internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml +++ /dev/null @@ -1,438 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - pdok.nl/wms-service-metadata-uuid: 70562932-e7dc-4ba2-ba4f-05863d02587c - creationTimestamp: null - labels: - dataset: luchtfotolabels - dataset-owner: bzk - service-type: wms - service-version: v1_0 - name: bzk-luchtfotolabels-v1-0 -spec: - healthCheck: - boundingbox: - maxx: "135531.2729437439411" - maxy: "457377.1306112145539" - minx: "135036.1077132325445" - miny: "456913.9317436855054" - horizontalPodAutoscalerPatch: - maxReplicas: 2 - minReplicas: 1 - options: - automaticCasing: false - disableWebserviceProxy: false - includeIngress: false - prefetchData: true - rewriteGroupToDataLayers: false - validateChildStyleNameEqual: false - validateRequests: false - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 6G - memory: 4G - requests: - cpu: "1" - ephemeral-storage: 6G - memory: 4G - service: - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden - gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - keywords: - - bzk - - luchtfotolabels - layer: - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden - gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - - bbox: - maxx: "795163" - maxy: "6181970" - minx: "-470271" - miny: "5562310" - crs: EPSG:25831 - - bbox: - maxx: "397827" - maxy: "6190420" - minx: "62461.6" - miny: "5565550" - crs: EPSG:25832 - - bbox: - maxx: "3220070" - maxy: "3840030" - minx: "2613360" - miny: "3509000" - crs: EPSG:3034 - - bbox: - maxx: "3644850" - maxy: "4155860" - minx: "3016760" - miny: "3812640" - crs: EPSG:3035 - - bbox: - maxx: "820873" - maxy: "7503110" - minx: "281318" - miny: "6483220" - crs: EPSG:3857 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4258 - - bbox: - maxx: "55.7212" - maxy: "7.37403" - minx: "50.2129" - miny: "2.52713" - crs: EPSG:4326 - - bbox: - maxx: "7.37403" - maxy: "55.7212" - minx: "2.52713" - miny: "50.2129" - crs: CRS:84 - keywords: - - bzk - - luchtfotolabels - layers: - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - layers: - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/100pixkm_luforoads/100pixkm_luforoads.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "48001" - minscaledenominator: "24001" - name: luchtfotoroads_100pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: roads.style - title: Luchtfoto roads 100pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/200pixkm_luforoads/200pixkm_luforoads.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "2.0" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "24001" - minscaledenominator: "12001" - name: luchtfotoroads_200pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: roads.style - title: Luchtfoto roads 200pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/400pixkm_luforoads/400pixkm_luforoads.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "2.5" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "12001" - minscaledenominator: "6001" - name: luchtfotoroads_400pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: roads.style - title: Luchtfoto roads 400pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/800pixkm_luforoads/800pixkm_luforoads.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "6001" - minscaledenominator: "3001" - name: luchtfotoroads_800pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: roads.style - title: Luchtfoto roads 800pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/1600pixkm_luforoads/1600pixkm_luforoads.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "3001" - minscaledenominator: "1501" - name: luchtfotoroads_1600pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: roads.style - title: Luchtfoto roads 1600pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/100pixkm_lufolabels/100pixkm_lufolabels.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "48001" - minscaledenominator: "24001" - name: luchtfotolabels_100pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: labels.style - title: Luchtfoto labels 100pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/200pixkm_lufolabels/200pixkm_lufolabels.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "24001" - minscaledenominator: "12001" - name: luchtfotolabels_200pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: labels.style - title: Luchtfoto labels 200pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/400pixkm_lufolabels/400pixkm_lufolabels.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "12001" - minscaledenominator: "6001" - name: luchtfotolabels_400pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: labels.style - title: Luchtfoto labels 400pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/800pixkm_lufolabels/800pixkm_lufolabels.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "6001" - minscaledenominator: "3001" - name: luchtfotolabels_800pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: labels.style - title: Luchtfoto labels 800pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/1600pixkm_lufolabels/1600pixkm_lufolabels.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "3001" - minscaledenominator: "1501" - name: luchtfotolabels_1600pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: labels.style - title: Luchtfoto labels 1600pixkm - visible: true - - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen - worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. - authority: - name: kadaster - spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 - url: http://www.kadaster.nl - data: - tif: - blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/3200pixkm_lufolabels/3200pixkm_lufolabels.vrt - offsite: '#978E97' - resample: BILINEAR - oversampleRatio: "1" - datasetMetadataUrl: - csw: - metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 - keywords: - - bzk - - luchtfotolabels - maxscaledenominator: "1501" - name: luchtfotolabels_3200pixkm - styles: - - name: luchtfotolabels - title: Luchtfotolabels - visualization: labels.style - title: Luchtfoto labels 3200pixkm - visible: true - name: lufolabels - styles: - - name: luchtfotolabels - title: Luchtfotolabels - title: Luchtfoto labels - visible: true - title: Luchtfoto Labels WMS - visible: true - ownerInfoRef: pdok - prefix: luchtfotolabels - stylingAssets: - configMapRefs: - - keys: - - roads.style - - labels.style - name: ${INCLUDES} - title: Luchtfoto Labels WMS - url: https://service.pdok.nl/bzk/luchtfotolabels/wms/v1_0 diff --git a/internal/controller/mapfilegenerator/types.go b/internal/controller/mapfilegenerator/types.go deleted file mode 100644 index affd32d..0000000 --- a/internal/controller/mapfilegenerator/types.go +++ /dev/null @@ -1,136 +0,0 @@ -package mapfilegenerator - -import ( - "path" - "regexp" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" -) - -//nolint:tagliatelle -type BaseServiceInput struct { - Title string `json:"service_title"` - Abstract string `json:"service_abstract"` - Keywords string `json:"service_keywords"` - Extent string `json:"service_extent"` - NamespacePrefix string `json:"service_namespace_prefix"` - NamespaceURI string `json:"service_namespace_uri"` - OnlineResource string `json:"service_onlineresource"` - Path string `json:"service_path"` - MetadataID string `json:"service_metadata_id"` - DatasetOwner *string `json:"dataset_owner,omitempty"` - AuthorityURL *string `json:"authority_url,omitempty"` - AutomaticCasing bool `json:"automatic_casing"` - DataEPSG string `json:"data_epsg"` - EPSGList []string `json:"epsg_list"` - DebugLevel int `json:"service_debug_level,omitempty"` - AccessConstraints string `json:"service_accessconstraints"` -} - -//nolint:tagliatelle -type WFSInput struct { - BaseServiceInput - MaxFeatures string `json:"service_wfs_maxfeatures"` - Layers []WFSLayer `json:"layers"` -} - -//nolint:tagliatelle -type WMSInput struct { - BaseServiceInput - Layers []WMSLayer `json:"layers"` - GroupLayers []GroupLayer `json:"group_layers"` - Symbols []string `json:"symbols"` - Fonts *string `json:"fonts,omitempty"` - Templates string `json:"templates,omitempty"` - OutputFormatJpg string `json:"outputformat_jpg"` - OutputFormatPng string `json:"outputformat_png8"` - MaxSize string `json:"maxSize"` - TopLevelName string `json:"top_level_name,omitempty"` - Resolution string `json:"resolution,omitempty"` - DefResolution string `json:"defresolution,omitempty"` -} - -//nolint:tagliatelle -type BaseLayer struct { - Name string `json:"name"` - Title string `json:"title"` - Abstract string `json:"abstract"` - Keywords string `json:"keywords"` - Extent string `json:"layer_extent"` - MetadataID string `json:"dataset_metadata_id"` - Columns []Column `json:"columns,omitempty"` - GeometryType *string `json:"geometry_type,omitempty"` - GeopackagePath *string `json:"gpkg_path,omitempty"` - TableName *string `json:"tablename,omitempty"` - Postgis *bool `json:"postgis,omitempty"` - MinScale *string `json:"minscale,omitempty"` - MaxScale *string `json:"maxscale,omitempty"` - TifPath *string `json:"tif_path,omitempty"` - Resample *string `json:"resample,omitempty"` - OversampleRatio *string `json:"oversample_ratio,omitempty"` - LabelNoClip bool `json:"label_no_clip,omitempty"` -} - -type WFSLayer struct { - BaseLayer -} - -//nolint:tagliatelle -type GroupLayer struct { - Name string `json:"name"` - Title string `json:"title"` - Abstract string `json:"abstract"` - StyleName string `json:"style_name"` - StyleTitle string `json:"style_title"` -} - -//nolint:tagliatelle -type WMSLayer struct { - BaseLayer - GroupName string `json:"group_name,omitempty"` - Styles []Style `json:"styles"` - Offsite string `json:"offsite,omitempty"` - GetFeatureInfoIncludesClass *bool `json:"get_feature_info_includes_class,omitempty"` -} - -type Column struct { - Name string `json:"name"` - Alias *string `json:"alias,omitempty"` -} - -type Style struct { - Path string `json:"path"` - Title string `json:"title,omitempty"` -} - -func SetDataFields[O pdoknlv3.WMSWFS](obj O, wmsLayer *WMSLayer, data pdoknlv3.Data) { - switch { - case data.Gpkg != nil: - gpkg := data.Gpkg - - wmsLayer.GeometryType = &gpkg.GeometryType - geopackageConstructedPath := "/srv/data/gpkg/" + path.Base(gpkg.BlobKey) - if !obj.Options().PrefetchData { - reReplace := regexp.MustCompile(`$[a-zA-Z0-9_]*]/`) - geopackageConstructedPath = path.Join("/vsiaz/geopackages", reReplace.ReplaceAllString(gpkg.BlobKey, "")) - } - wmsLayer.GeopackagePath = &geopackageConstructedPath - case data.TIF != nil: - tif := data.TIF - wmsLayer.GeometryType = smoothoperatorutils.Pointer("Raster") - wmsLayer.BaseLayer.TifPath = smoothoperatorutils.Pointer(path.Join(tifPath, path.Base(tif.BlobKey))) - if !obj.Options().PrefetchData { - reReplace := regexp.MustCompile(`$[a-zA-Z0-9_]*]/`) - wmsLayer.BaseLayer.TifPath = smoothoperatorutils.Pointer(path.Join("/vsiaz", reReplace.ReplaceAllString(tif.BlobKey, ""))) - } - wmsLayer.BaseLayer.Resample = &tif.Resample - wmsLayer.BaseLayer.OversampleRatio = &tif.OversampleRatio - wmsLayer.Offsite = smoothoperatorutils.PointerVal(tif.Offsite, "") - wmsLayer.GetFeatureInfoIncludesClass = &tif.GetFeatureInfoIncludesClass - case data.Postgis != nil: - postgis := data.Postgis - wmsLayer.Postgis = smoothoperatorutils.Pointer(true) - wmsLayer.GeometryType = &postgis.GeometryType - } -} diff --git a/internal/controller/mapperutils/utils.go b/internal/controller/mapperutils/utils.go deleted file mode 100644 index 675b651..0000000 --- a/internal/controller/mapperutils/utils.go +++ /dev/null @@ -1,72 +0,0 @@ -package mapperutils - -import ( - "strings" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - corev1 "k8s.io/api/core/v1" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -func GetContainerResourceRequest[O pdoknlv3.WMSWFS](obj O, containerName string, resource corev1.ResourceName) *resource.Quantity { - for _, container := range obj.PodSpecPatch().Containers { - if container.Name == containerName { - q := container.Resources.Requests[resource] - if !q.IsZero() { - return &q - } - } - } - - return nil -} - -func GetContainerResourceLimit[O pdoknlv3.WMSWFS](obj O, containerName string, resource corev1.ResourceName) *resource.Quantity { - for _, container := range obj.PodSpecPatch().Containers { - if container.Name == containerName { - q := container.Resources.Limits[resource] - if !q.IsZero() { - return &q - } - } - } - - return nil -} - -// Use ephemeral volume when ephemeral storage is greater then 10Gi -func UseEphemeralVolume[O pdoknlv3.WMSWFS](obj O) (bool, *resource.Quantity) { - value := EphemeralStorageLimit(obj) - threshold := resource.MustParse("10Gi") - - if value != nil { - return value.Value() > threshold.Value(), value - } - - return false, nil -} - -func EphemeralStorageLimit[O pdoknlv3.WMSWFS](obj O) *resource.Quantity { - return GetContainerResourceLimit(obj, constants.MapserverName, corev1.ResourceEphemeralStorage) -} - -func EphemeralStorageRequest[O pdoknlv3.WMSWFS](obj O) *resource.Quantity { - return GetContainerResourceRequest(obj, constants.MapserverName, corev1.ResourceEphemeralStorage) -} - -func GetNamespaceURI(prefix string, ownerInfo *smoothoperatorv1.OwnerInfo) string { - return strings.ReplaceAll(*ownerInfo.Spec.NamespaceTemplate, "{{prefix}}", prefix) -} - -func AnyMatch[S ~[]E, E any](slice S, eql func(E) bool) bool { - for _, elem := range slice { - if eql(elem) { - return true - } - } - return false -} diff --git a/internal/controller/mapserver/deployment.go b/internal/controller/mapserver/deployment.go deleted file mode 100644 index b8f4d30..0000000 --- a/internal/controller/mapserver/deployment.go +++ /dev/null @@ -1,200 +0,0 @@ -package mapserver - -import ( - "errors" - "strings" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - "github.com/pdok/mapserver-operator/internal/controller/utils" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/static" - "github.com/pdok/mapserver-operator/internal/controller/types" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -const mimeTextXML = "text/xml" - -func GetMapserverContainer[O pdoknlv3.WMSWFS](obj O, images types.Images) (*corev1.Container, error) { - livenessProbe, readinessProbe, startupProbe, err := getProbes(obj) - if err != nil { - return nil, err - } - - container := corev1.Container{ - Name: constants.MapserverName, - Image: images.MapserverImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Ports: []corev1.ContainerPort{{ContainerPort: constants.MapserverPortNr, Protocol: corev1.ProtocolTCP}}, - Env: []corev1.EnvVar{ - { - Name: "SERVICE_TYPE", - Value: string(obj.Type()), - }, - { - Name: "MAPSERVER_CONFIG_FILE", - Value: "/srv/mapserver/config/default_mapserver.conf", - }, - GetMapfileEnvVar(obj), - }, - VolumeMounts: getVolumeMounts(obj.Mapfile() != nil), - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("800M"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("0.15"), - }, - }, - Lifecycle: &corev1.Lifecycle{PreStop: &corev1.LifecycleHandler{Exec: &corev1.ExecAction{Command: []string{"sleep", "15"}}}}, - StartupProbe: startupProbe, - ReadinessProbe: readinessProbe, - LivenessProbe: livenessProbe, - } - - if obj.Type() == pdoknlv3.ServiceTypeWMS && !obj.Options().DisableWebserviceProxy { - container.Resources.Requests[corev1.ResourceCPU] = resource.MustParse("0.1") - } - - return &container, nil -} - -func getVolumeMounts(customMapfile bool) []corev1.VolumeMount { - volumeMounts := []corev1.VolumeMount{ - utils.GetBaseVolumeMount(), - utils.GetDataVolumeMount(), - } - - staticFiles, _ := static.GetStaticFiles() - for _, name := range staticFiles { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: constants.MapserverName, - MountPath: "/srv/mapserver/config/" + name, - SubPath: name, - }) - } - if customMapfile { - volumeMounts = append(volumeMounts, utils.GetMapfileVolumeMount()) - } - - return volumeMounts -} - -func GetMapfileEnvVar[O pdoknlv3.WMSWFS](obj O) corev1.EnvVar { - mapFileName := "service.map" - if obj.Mapfile() != nil { - mapFileName = obj.Mapfile().ConfigMapKeyRef.Key - } - - return corev1.EnvVar{ - Name: "MS_MAPFILE", - Value: "/srv/data/config/mapfile/" + mapFileName, - } -} - -func getProbes[O pdoknlv3.WMSWFS](obj O) (livenessProbe *corev1.Probe, readinessProbe *corev1.Probe, startupProbe *corev1.Probe, err error) { - livenessProbe = getLivenessProbe(obj) - switch obj.Type() { - case pdoknlv3.ServiceTypeWFS: - wfs, _ := any(obj).(*pdoknlv3.WFS) - readinessProbe, err = getReadinessProbeForWFS(wfs) - if err != nil { - return nil, nil, nil, err - } - startupProbe, err = getStartupProbeForWFS(wfs) - if err != nil { - return nil, nil, nil, err - } - case pdoknlv3.ServiceTypeWMS: - wms, _ := any(obj).(*pdoknlv3.WMS) - readinessProbe, err = getReadinessProbeForWMS(wms) - if err != nil { - return nil, nil, nil, err - } - startupProbe, err = getStartupProbeForWMS(wms) - if err != nil { - return nil, nil, nil, err - } - } - return -} - -func getLivenessProbe[O pdoknlv3.WMSWFS](obj O) *corev1.Probe { - queryString := "SERVICE=" + string(obj.Type()) + "&request=GetCapabilities" - return getProbe(queryString, mimeTextXML) -} - -func getReadinessProbeForWFS(wfs *pdoknlv3.WFS) (*corev1.Probe, error) { - queryString, mime, err := wfs.ReadinessQueryString() - if err != nil { - return nil, err - } - return getProbe(queryString, mime), nil -} - -func getReadinessProbeForWMS(wms *pdoknlv3.WMS) (*corev1.Probe, error) { - queryString, mime, err := wms.ReadinessQueryString() - if err != nil { - return nil, err - } - - return getProbe(queryString, mime), nil -} - -func getStartupProbeForWFS(wfs *pdoknlv3.WFS) (*corev1.Probe, error) { - if hc := wfs.Spec.HealthCheck; hc != nil { - return getProbe(hc.Querystring, hc.Mimetype), nil - } - - var typeNames []string - for _, ft := range wfs.Spec.Service.FeatureTypes { - typeNames = append(typeNames, ft.Name) - } - if len(typeNames) == 0 { - return nil, errors.New("cannot get startup probe for WFS, featuretypes could not be found") - } - - queryString := "SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=" + strings.Join(typeNames, ",") + "&STARTINDEX=0&COUNT=1" - return getProbe(queryString, mimeTextXML), nil -} - -func getStartupProbeForWMS(wms *pdoknlv3.WMS) (*corev1.Probe, error) { - if hc := wms.Spec.HealthCheck; hc != nil && hc.Querystring != nil { - return getProbe(*hc.Querystring, *hc.Mimetype), nil - } - - var layerNames []string - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if layer.Name != nil { - layerNames = append(layerNames, *layer.Name) - } - - } - if len(layerNames) == 0 { - return nil, errors.New("cannot get startup probe for WMS, layers could not be found") - } - - queryString := "SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=" + wms.HealthCheckBBox() + "&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=" + strings.Join(layerNames, ",") + "&STYLES=&FORMAT=image/png" - mimeType := "image/png" - return getProbe(queryString, mimeType), nil -} - -func getProbe(queryString string, mimeType string) *corev1.Probe { - probeCmd := "wget -SO- -T 10 -t 2 'http://127.0.0.1:80/mapserver?" + queryString + "' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i 'Content-Type: " + mimeType + "'" - return &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{Exec: &corev1.ExecAction{ - Command: []string{ - "/bin/sh", - "-c", - probeCmd, - }, - }}, - SuccessThreshold: 1, - FailureThreshold: 3, - InitialDelaySeconds: 20, - PeriodSeconds: 10, - TimeoutSeconds: 10, - } -} diff --git a/internal/controller/mapserver/deployment_test.go b/internal/controller/mapserver/deployment_test.go deleted file mode 100644 index 7467e8f..0000000 --- a/internal/controller/mapserver/deployment_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package mapserver - -import ( - "testing" - - "github.com/pdok/mapserver-operator/api/v2beta1" - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/yaml" - - _ "embed" -) - -//go:embed test_data/expected_volumemounts.yaml -var expectedVolumeMountsYaml []byte - -func TestGetVolumeMounts(t *testing.T) { - pdoknlv3.SetHost("https://service.pdok.nl") - result := getVolumeMounts(false) - - var expectedVolumeMounts struct{ VolumeMounts []corev1.VolumeMount } - err := yaml.Unmarshal(expectedVolumeMountsYaml, &expectedVolumeMounts) - assert.NoError(t, err) - assert.Equal(t, expectedVolumeMounts.VolumeMounts, result) -} - -//go:embed test_data/expected_livenessprobe.yaml -var expectedLivenessProbe []byte - -//go:embed test_data/expected_readinessprobe.yaml -var expectedReadinessProbe []byte - -//go:embed test_data/expected_startupprobe.yaml -var expectedStartupProbe []byte - -func TestGetProbesForDeployment(t *testing.T) { - var wfs = getV3() - pdoknlv3.SetHost("https://service.pdok.nl") - livenessResult, readinessResult, startupResult, err := getProbes(wfs) - assert.NoError(t, err) - - var expectedLiveness corev1.Probe - var expectedReadiness corev1.Probe - var expectedStartup corev1.Probe - err = yaml.Unmarshal(expectedLivenessProbe, &expectedLiveness) - assert.NoError(t, err) - err = yaml.Unmarshal(expectedReadinessProbe, &expectedReadiness) - assert.NoError(t, err) - err = yaml.Unmarshal(expectedStartupProbe, &expectedStartup) - assert.NoError(t, err) - assert.Equal(t, &expectedLiveness, livenessResult) - assert.Equal(t, &expectedReadiness, readinessResult) - assert.Equal(t, &expectedStartup, startupResult) -} - -//go:embed test_data/v2_input.yaml -var v2Input []byte - -func getV3() *pdoknlv3.WFS { - var v2wfs v2beta1.WFS - err := yaml.Unmarshal(v2Input, &v2wfs) - if err != nil { - panic(err) - } - var wfs pdoknlv3.WFS - _ = v2wfs.ToV3(&wfs) - return &wfs -} diff --git a/internal/controller/mapserver/test_data/expected_livenessprobe.yaml b/internal/controller/mapserver/test_data/expected_livenessprobe.yaml deleted file mode 100644 index 49e53e5..0000000 --- a/internal/controller/mapserver/test_data/expected_livenessprobe.yaml +++ /dev/null @@ -1,11 +0,0 @@ -exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' -failureThreshold: 3 -initialDelaySeconds: 20 -periodSeconds: 10 -successThreshold: 1 -timeoutSeconds: 10 diff --git a/internal/controller/mapserver/test_data/expected_readinessprobe.yaml b/internal/controller/mapserver/test_data/expected_readinessprobe.yaml deleted file mode 100644 index 5eec511..0000000 --- a/internal/controller/mapserver/test_data/expected_readinessprobe.yaml +++ /dev/null @@ -1,11 +0,0 @@ -exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=wegvakken&STARTINDEX=0&COUNT=1'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' -failureThreshold: 3 -initialDelaySeconds: 20 -periodSeconds: 10 -successThreshold: 1 -timeoutSeconds: 10 diff --git a/internal/controller/mapserver/test_data/expected_startupprobe.yaml b/internal/controller/mapserver/test_data/expected_startupprobe.yaml deleted file mode 100644 index 4297a3d..0000000 --- a/internal/controller/mapserver/test_data/expected_startupprobe.yaml +++ /dev/null @@ -1,11 +0,0 @@ -exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=wegvakken,hectopunten&STARTINDEX=0&COUNT=1'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' -failureThreshold: 3 -initialDelaySeconds: 20 -periodSeconds: 10 -successThreshold: 1 -timeoutSeconds: 10 diff --git a/internal/controller/mapserver/test_data/expected_volumemounts.yaml b/internal/controller/mapserver/test_data/expected_volumemounts.yaml deleted file mode 100644 index f0939e9..0000000 --- a/internal/controller/mapserver/test_data/expected_volumemounts.yaml +++ /dev/null @@ -1,18 +0,0 @@ -volumeMounts: -- mountPath: /srv/data - name: base - readOnly: false -- mountPath: /var/www - name: data -- mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf -- mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua -- mountPath: /srv/mapserver/config/default_mapserver.conf - name: mapserver - subPath: default_mapserver.conf -- mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml diff --git a/internal/controller/mapserver/test_data/v2_input.yaml b/internal/controller/mapserver/test_data/v2_input.yaml deleted file mode 100644 index d4dfc87..0000000 --- a/internal/controller/mapserver/test_data/v2_input.yaml +++ /dev/null @@ -1,162 +0,0 @@ -apiVersion: pdok.nl/v2beta1 -kind: WFS -metadata: - name: rws-nwbwegen-v1-0 - labels: - dataset-owner: rws - dataset: nwbwegen - service-version: v1_0 - service-type: wfs - annotations: - lifecycle-phase: prod - service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb -spec: - general: - datasetOwner: rws - dataset: nwbwegen - serviceVersion: v1_0 - kubernetes: - resources: - limits: - ephemeralStorage: 505Mi - requests: - ephemeralStorage: 255Mi - service: - title: NWB - Wegen WFS - abstract: - Dit is de web feature service van het Nationaal Wegen Bestand (NWB) - - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal - Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in - Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als - het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze - zijn voorzien van een straatnaam of nummer. - inspire: true - metadataIdentifier: a9fa7fff-6365-4885-950c-e9d9848359ee - authority: - name: rws - url: https://www.rijkswaterstaat.nl - dataEPSG: EPSG:28992 - extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - - Hectometerpunten - - HVD - - Mobiliteit - featureTypes: - - name: wegvakken - title: Wegvakken - abstract: - Dit featuretype bevat de wegvakken uit het Nationaal Wegen bestand - (NWB) en bevat gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, - routenummer, wegbeheerder, huisnummers, enz. - sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Wegvakken - data: - gpkg: - table: wegvakken - geometryType: MultiLineString - blobKey: geopackages/rws/nwbwegen/1c56dc48-2cf4-4631-8b09-ed385d5368d1/1/nwb_wegen.gpkg - columns: - - fid - - objectid - - wvk_id - - wvk_begdat - - jte_id_beg - - jte_id_end - - wegbehsrt - - wegnummer - - wegdeelltr - - hecto_lttr - - bst_code - - rpe_code - - admrichtng - - rijrichtng - - stt_naam - - stt_bron - - wpsnaam - - gme_id - - gme_naam - - hnrstrlnks - - hnrstrrhts - - e_hnr_lnks - - e_hnr_rhts - - l_hnr_lnks - - l_hnr_rhts - - begafstand - - endafstand - - beginkm - - eindkm - - pos_tv_wol - - wegbehcode - - wegbehnaam - - distrcode - - distrnaam - - dienstcode - - dienstnaam - - wegtype - - wgtype_oms - - routeltr - - routenr - - routeltr2 - - routenr2 - - routeltr3 - - routenr3 - - routeltr4 - - routenr4 - - wegnr_aw - - wegnr_hmp - - geobron_id - - geobron_nm - - bronjaar - - openlr - - bag_orl - - frc - - fow - - alt_naam - - alt_nr - - rel_hoogte - - st_lengthshape - - name: hectopunten - title: Hectopunten - abstract: - Dit featuretype bevat de hectopunten uit het Nationaal Wegen Bestand - (NWB) en bevat gedetailleerde informatie per hectopunt zoals hectometrering, - afstand, zijde en hectoletter. - sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff - datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 - keywords: - - Vervoersnetwerken - - Menselijke gezondheid en veiligheid - - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) - - Nationaal - - Voertuigen - - Verkeer - - Hectometerpunten - data: - gpkg: - blobKey: geopackages/rws/nwbwegen/1c56dc48-2cf4-4631-8b09-ed385d5368d1/1/nwb_wegen.gpkg - columns: - - fid - - objectid - - hectomtrng - - afstand - - wvk_id - - wvk_begdat - - zijde - - hecto_lttr - geometryType: MultiPoint - table: hectopunten diff --git a/internal/controller/middleware.go b/internal/controller/middleware.go deleted file mode 100644 index 67efa86..0000000 --- a/internal/controller/middleware.go +++ /dev/null @@ -1,48 +0,0 @@ -package controller - -import ( - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - "github.com/traefik/traefik/v3/pkg/config/dynamic" - traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" -) - -const corsHeadersName = "mapserver-headers" - -func getBareCorsHeadersMiddleware[O pdoknlv3.WMSWFS](obj O) *traefikiov1alpha1.Middleware { - return &traefikiov1alpha1.Middleware{ - ObjectMeta: metav1.ObjectMeta{ - Name: getSuffixedName(obj, corsHeadersName), - // name might become too long. not handling here. will just fail on apply. - Namespace: obj.GetNamespace(), - UID: obj.GetUID(), - }, - } -} - -func mutateCorsHeadersMiddleware[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, middleware *traefikiov1alpha1.Middleware) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, middleware, labels); err != nil { - return err - } - middleware.Spec = traefikiov1alpha1.MiddlewareSpec{ - Headers: &dynamic.Headers{ - CustomResponseHeaders: map[string]string{ - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Method": "GET, POST, OPTIONS", - "Access-Control-Allow-Origin": "*", - "Cache-Control": "public, max-age=3600, no-transform", - }, - }, - } - - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, middleware, middleware); err != nil { - return err - } - - return ctrl.SetControllerReference(obj, middleware, getReconcilerScheme(r)) -} diff --git a/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go b/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go deleted file mode 100644 index 408d83d..0000000 --- a/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go +++ /dev/null @@ -1,93 +0,0 @@ -package ogcwebserviceproxy - -import ( - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/constants" - "github.com/pdok/mapserver-operator/internal/controller/types" - "github.com/pdok/mapserver-operator/internal/controller/utils" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - yaml "sigs.k8s.io/yaml/goyaml.v3" -) - -func GetOgcWebserviceProxyContainer(wms *pdoknlv3.WMS, images types.Images) (*corev1.Container, error) { - container := corev1.Container{ - Name: constants.OgcWebserviceProxyName, - Image: images.OgcWebserviceProxyImage, - ImagePullPolicy: corev1.PullIfNotPresent, - Ports: []corev1.ContainerPort{{ContainerPort: 9111}}, - Command: getCommand(wms), - VolumeMounts: []corev1.VolumeMount{ - utils.GetConfigVolumeMount(constants.ConfigMapOgcWebserviceProxyVolumeName), - }, - Resources: corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("200M"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("0.05"), - }, - }, - } - return &container, nil -} - -func getCommand(wms *pdoknlv3.WMS) []string { - command := []string{ - "/ogc-webservice-proxy", - "-h=http://127.0.0.1/", - "-t=wms", - "-s=/input/service-config.yaml", - } - - if wms.Options().ValidateRequests { - command = append(command, "-v") - } - if wms.Options().RewriteGroupToDataLayers { - command = append(command, "-r") - } - - command = append(command, "-d=15") - return command - -} - -func GetConfig(wms *pdoknlv3.WMS) (config string, err error) { - input, err := MapWMSToOgcWebserviceProxyConfig(wms) - if err != nil { - return "", err - } - - yamlConfig, err := yaml.Marshal(input) - if err != nil { - return "", err - } - return string(yamlConfig), nil -} - -func MapWMSToOgcWebserviceProxyConfig(wms *pdoknlv3.WMS) (config Config, err error) { - dataLayersForGroupLayer := func(l pdoknlv3.Layer) []string { - var dataLayers []string - for _, childLayer := range l.GetAllSublayers() { - if childLayer.IsDataLayer() { - dataLayers = append(dataLayers, *childLayer.Name) - } - } - return dataLayers - } - - config.GroupLayers = make(map[string][]string) - for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { - if !layer.IsTopLayer && layer.IsGroupLayer && layer.Name != nil { - config.GroupLayers[*layer.Name] = dataLayersForGroupLayer(layer.Layer) - } - } - if wms.Spec.Service.Layer.Name != nil { - config.GroupLayers[*wms.Spec.Service.Layer.Name] = dataLayersForGroupLayer(wms.Spec.Service.Layer) - } - return -} - -type Config struct { - GroupLayers map[string][]string `yaml:"grouplayers"` -} diff --git a/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go b/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go deleted file mode 100644 index 3cda6b2..0000000 --- a/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package ogcwebserviceproxy - -import ( - "os" - "testing" - - "github.com/google/go-cmp/cmp" - "sigs.k8s.io/yaml" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" -) - -func TestGetConfig(t *testing.T) { - tests := []string{"named-toplayer", "unnamed-toplayer"} - - for _, tt := range tests { - input, err := os.ReadFile("test_data/input/" + tt + ".yaml") - if err != nil { - t.Errorf("os.ReadFile() error = %v", err) - } - wms := &pdoknlv3.WMS{} - if err := yaml.Unmarshal(input, wms); err != nil { - t.Errorf("yaml.Unmarshal() error = %v", err) - } - - generated, err := MapWMSToOgcWebserviceProxyConfig(wms) - if err != nil { - t.Errorf("MapWMSToOgcWebserviceProxyConfig() error = %v", err) - } - - expectedBytes, err := os.ReadFile("test_data/expected/" + tt + ".yaml") - if err != nil { - t.Errorf("os.ReadFile() error = %v", err) - } - - var expected Config - if err := yaml.Unmarshal(expectedBytes, &expected); err != nil { - t.Errorf("yaml.Unmarshal() error = %v", err) - } - - diff := cmp.Diff(expected, generated) - if diff != "" { - t.Errorf("GetConfig() mismatch (-want +got):\n%s", diff) - } - } -} diff --git a/internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml deleted file mode 100644 index e867543..0000000 --- a/internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml +++ /dev/null @@ -1,12 +0,0 @@ -grouplayers: - grouplayer-1: - - datalayer-1 - - datalayer-2 - grouplayer-2: - - datalayer-3 - - datalayer-4 - toplayer: - - datalayer-1 - - datalayer-2 - - datalayer-3 - - datalayer-4 \ No newline at end of file diff --git a/internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml deleted file mode 100644 index d1b7dee..0000000 --- a/internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml +++ /dev/null @@ -1,7 +0,0 @@ -grouplayers: - grouplayer-1: - - datalayer-1 - - datalayer-2 - grouplayer-2: - - datalayer-3 - - datalayer-4 \ No newline at end of file diff --git a/internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml deleted file mode 100644 index 8df4892..0000000 --- a/internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml +++ /dev/null @@ -1,52 +0,0 @@ -metadata: -spec: - service: - abstract: "" - dataEPSG: "" - keywords: null - layer: - layers: - - layers: - - data: - gpkg: - blobKey: blob-1 - columns: null - geometryType: "" - tableName: "" - name: datalayer-1 - visible: false - - data: - gpkg: - blobKey: blob-2 - columns: null - geometryType: "" - tableName: "" - name: datalayer-2 - visible: false - name: grouplayer-1 - visible: false - - layers: - - data: - gpkg: - blobKey: blob-3 - columns: null - geometryType: "" - tableName: "" - name: datalayer-3 - visible: false - - data: - gpkg: - blobKey: blob-4 - columns: null - geometryType: "" - tableName: "" - name: datalayer-4 - visible: false - name: grouplayer-2 - visible: false - name: toplayer - visible: false - ownerInfoRef: "" - prefix: "" - title: "" - url: "http://test.test/test" diff --git a/internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml deleted file mode 100644 index f34f687..0000000 --- a/internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml +++ /dev/null @@ -1,51 +0,0 @@ -metadata: -spec: - service: - abstract: "" - dataEPSG: "" - keywords: null - layer: - layers: - - layers: - - data: - gpkg: - blobKey: blob-1 - columns: null - geometryType: "" - tableName: "" - name: datalayer-1 - visible: false - - data: - gpkg: - blobKey: blob-2 - columns: null - geometryType: "" - tableName: "" - name: datalayer-2 - visible: false - name: grouplayer-1 - visible: false - - layers: - - data: - gpkg: - blobKey: blob-3 - columns: null - geometryType: "" - tableName: "" - name: datalayer-3 - visible: false - - data: - gpkg: - blobKey: blob-4 - columns: null - geometryType: "" - tableName: "" - name: datalayer-4 - visible: false - name: grouplayer-2 - visible: false - visible: false - ownerInfoRef: "" - prefix: "" - title: "" - url: "http://test.test/test" diff --git a/internal/controller/poddisruptionbudget.go b/internal/controller/poddisruptionbudget.go deleted file mode 100644 index 6bd848b..0000000 --- a/internal/controller/poddisruptionbudget.go +++ /dev/null @@ -1,42 +0,0 @@ -package controller - -import ( - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/constants" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - ctrl "sigs.k8s.io/controller-runtime" -) - -func getBarePodDisruptionBudget[O pdoknlv3.WMSWFS](obj O) *policyv1.PodDisruptionBudget { - return &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: getSuffixedName(obj, constants.MapserverName), - Namespace: obj.GetNamespace(), - }, - } -} - -func mutatePodDisruptionBudget[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, podDisruptionBudget *policyv1.PodDisruptionBudget) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, podDisruptionBudget, labels); err != nil { - return err - } - - matchLabels := smoothoperatorutils.CloneOrEmptyMap(labels) - podDisruptionBudget.Spec = policyv1.PodDisruptionBudgetSpec{ - MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, - Selector: &metav1.LabelSelector{ - MatchLabels: matchLabels, - }, - } - - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, podDisruptionBudget, podDisruptionBudget); err != nil { - return err - } - return ctrl.SetControllerReference(obj, podDisruptionBudget, getReconcilerScheme(r)) -} diff --git a/internal/controller/reconciler.go b/internal/controller/reconciler.go deleted file mode 100644 index bba708e..0000000 --- a/internal/controller/reconciler.go +++ /dev/null @@ -1,45 +0,0 @@ -package controller - -import ( - "github.com/pdok/mapserver-operator/internal/controller/types" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type Reconciler interface { - *WFSReconciler | *WMSReconciler - client.StatusClient -} - -func getReconcilerClient[R Reconciler](r R) client.Client { - switch any(r).(type) { - case *WFSReconciler: - return any(r).(*WFSReconciler).Client - case *WMSReconciler: - return any(r).(*WMSReconciler).Client - } - - return nil -} - -func getReconcilerScheme[R Reconciler](r R) *runtime.Scheme { - switch any(r).(type) { - case *WFSReconciler: - return any(r).(*WFSReconciler).Scheme - case *WMSReconciler: - return any(r).(*WMSReconciler).Scheme - } - - return nil -} - -func getReconcilerImages[R Reconciler](r R) *types.Images { - switch any(r).(type) { - case *WFSReconciler: - return &any(r).(*WFSReconciler).Images - case *WMSReconciler: - return &any(r).(*WMSReconciler).Images - } - - return nil -} diff --git a/internal/controller/service.go b/internal/controller/service.go deleted file mode 100644 index fae666d..0000000 --- a/internal/controller/service.go +++ /dev/null @@ -1,77 +0,0 @@ -package controller - -import ( - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/constants" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - ctrl "sigs.k8s.io/controller-runtime" -) - -const ( - mapserverWebserviceProxyPortNr = 9111 - metricPortName = "metric" -) - -func getBareService[O pdoknlv3.WMSWFS](obj O) *corev1.Service { - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: getSuffixedName(obj, constants.MapserverName), - Namespace: obj.GetNamespace(), - }, - } -} - -func mutateService[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, service *corev1.Service) error { - reconcilerClient := getReconcilerClient(r) - - labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) - selector := smoothoperatorutils.CloneOrEmptyMap(labels) - if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, service, labels); err != nil { - return err - } - - ports := []corev1.ServicePort{ - { - Name: constants.MapserverName, - Port: constants.MapserverPortNr, - TargetPort: intstr.FromInt32(constants.MapserverPortNr), - Protocol: corev1.ProtocolTCP, - }, - } - - if obj.Type() == pdoknlv3.ServiceTypeWMS { - if obj.Options().UseWebserviceProxy() { - ports = append(ports, corev1.ServicePort{ - Name: constants.OgcWebserviceProxyName, - Port: 9111, - }) - } - } - - // Add port here to get the same port order as the odl ansible operator - ports = append(ports, corev1.ServicePort{ - Name: metricPortName, - Port: constants.ApachePortNr, - TargetPort: intstr.FromInt32(constants.ApachePortNr), - Protocol: corev1.ProtocolTCP, - }) - - service.Spec = corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - ClusterIP: service.Spec.ClusterIP, - ClusterIPs: service.Spec.ClusterIPs, - IPFamilyPolicy: service.Spec.IPFamilyPolicy, - IPFamilies: service.Spec.IPFamilies, - SessionAffinity: corev1.ServiceAffinityNone, - InternalTrafficPolicy: smoothoperatorutils.Pointer(corev1.ServiceInternalTrafficPolicyCluster), - Ports: ports, - Selector: selector, - } - if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, service, service); err != nil { - return err - } - return ctrl.SetControllerReference(obj, service, getReconcilerScheme(r)) -} diff --git a/internal/controller/shared_controller.go b/internal/controller/shared_controller.go deleted file mode 100644 index b70d5a7..0000000 --- a/internal/controller/shared_controller.go +++ /dev/null @@ -1,306 +0,0 @@ -package controller - -import ( - "context" - "fmt" - "strconv" - "strings" - "time" - - ctrl "sigs.k8s.io/controller-runtime" - - "github.com/pkg/errors" - - traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" - appsv1 "k8s.io/api/apps/v1" - autoscalingv2 "k8s.io/api/autoscaling/v2" - policyv1 "k8s.io/api/policy/v1" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/pdok/smooth-operator/model" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "github.com/pdok/mapserver-operator/internal/controller/types" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorstatus "github.com/pdok/smooth-operator/pkg/status" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -const ( - AppLabelKey = "pdok.nl/app" - InspireLabelKey = "pdok.nl/inspire" -) - -func createControllerManager(mgr ctrl.Manager, obj client.Object) *builder.TypedBuilder[reconcile.Request] { - var kind string - switch any(obj).(type) { - case *pdoknlv3.WMS: - kind = "WMS" - case *pdoknlv3.WFS: - kind = "WFS" - } - - controllerMgr := ctrl.NewControllerManagedBy(mgr).For(obj).Named(strings.ToLower(kind)) - controllerMgr.Owns(&corev1.ConfigMap{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&appsv1.Deployment{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&traefikiov1alpha1.Middleware{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&traefikiov1alpha1.IngressRoute{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&autoscalingv2.HorizontalPodAutoscaler{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&policyv1.PodDisruptionBudget{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). - Owns(&smoothoperatorv1.OwnerInfo{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})) - - return controllerMgr.Watches(&appsv1.ReplicaSet{}, smoothoperatorstatus.GetReplicaSetEventHandlerForObj(mgr, kind)) -} - -func ttlExpired[O pdoknlv3.WMSWFS](obj O) bool { - var lifecycle *model.Lifecycle - switch any(obj).(type) { - case *pdoknlv3.WFS: - wfs := any(obj).(*pdoknlv3.WFS) - lifecycle = wfs.Spec.Lifecycle - case *pdoknlv3.WMS: - wms := any(obj).(*pdoknlv3.WMS) - lifecycle = wms.Spec.Lifecycle - } - - if lifecycle != nil && lifecycle.TTLInDays != nil { - expiresAt := obj.GetCreationTimestamp().Add(time.Duration(*lifecycle.TTLInDays) * 24 * time.Hour) - - return expiresAt.Before(time.Now()) - } - - return false -} - -func ensureLabel[O pdoknlv3.WMSWFS](obj O, key, value string) { - labels := obj.GetLabels() - if _, ok := labels[key]; !ok { - labels[key] = value - } - - obj.SetLabels(labels) -} - -func getSuffixedName[O pdoknlv3.WMSWFS](obj O, suffix string) string { - return obj.TypedName() + "-" + suffix -} - -func addCommonLabels[O pdoknlv3.WMSWFS](obj O, labels map[string]string) map[string]string { - labels[AppLabelKey] = constants.MapserverName - - inspire := false - switch any(obj).(type) { - case *pdoknlv3.WFS: - inspire = any(obj).(*pdoknlv3.WFS).Spec.Service.Inspire != nil - case *pdoknlv3.WMS: - inspire = any(obj).(*pdoknlv3.WMS).Spec.Service.Inspire != nil - } - - labels[InspireLabelKey] = strconv.FormatBool(inspire) - - return labels -} - -func createOrUpdateAllForWMSWFS[R Reconciler, O pdoknlv3.WMSWFS](ctx context.Context, r R, obj O, ownerInfo *smoothoperatorv1.OwnerInfo) (operationResults map[string]controllerutil.OperationResult, err error) { - reconcilerClient := getReconcilerClient(r) - - hashedConfigMapNames, operationResults, err := createOrUpdateConfigMaps(ctx, r, obj, ownerInfo) - if err != nil { - return operationResults, err - } - - // region Deployment - { - deployment := getBareDeployment(obj) - operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, deployment)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, deployment, func() error { - return mutateDeployment(r, obj, deployment, hashedConfigMapNames) - }) - if err != nil && !strings.Contains(err.Error(), "the object has been modified; please apply your changes to the latest version and try again") { - return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, deployment), err) - } - } - // end region Deployment - - // region TraefikMiddleware - if obj.Options().IncludeIngress { - middleware := getBareCorsHeadersMiddleware(obj) - operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, middleware)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, middleware, func() error { - return mutateCorsHeadersMiddleware(r, obj, middleware) - }) - if err != nil { - return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, middleware), err) - } - } - // end region TraefikMiddleware - - // region PodDisruptionBudget - { - err = createOrUpdateOrDeletePodDisruptionBudget(ctx, r, obj, operationResults) - if err != nil { - return operationResults, err - } - } - // end region PodDisruptionBudget - - // region HorizontalAutoScaler - { - autoscaler := getBareHorizontalPodAutoScaler(obj) - operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, autoscaler)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, autoscaler, func() error { - return mutateHorizontalPodAutoscaler(r, obj, autoscaler) - }) - if err != nil { - return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, autoscaler), err) - } - } - // end region HorizontalAutoScaler - - // region IngressRoute - if obj.Options().IncludeIngress { - ingress := getBareIngressRoute(obj) - operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, ingress)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, ingress, func() error { - return mutateIngressRoute(r, obj, ingress) - }) - if err != nil { - return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, ingress), err) - } - } - // end region IngressRoute - - // region Service - { - service := getBareService(obj) - operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, service)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, service, func() error { - return mutateService(r, obj, service) - }) - if err != nil { - return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, service), err) - } - } - // end region Service - - return operationResults, nil -} - -func createOrUpdateConfigMaps[R Reconciler, O pdoknlv3.WMSWFS](ctx context.Context, r R, obj O, ownerInfo *smoothoperatorv1.OwnerInfo) (hashedConfigMapNames types.HashedConfigMapNames, operationResults map[string]controllerutil.OperationResult, err error) { - operationResults, configMaps := make(map[string]controllerutil.OperationResult), make(map[string]func(R, O, *corev1.ConfigMap) error) - configMaps[constants.MapserverName] = mutateConfigMap - if obj.Mapfile() == nil { - configMaps[constants.MapfileGeneratorName] = func(r R, o O, cm *corev1.ConfigMap) error { - return mutateConfigMapMapfileGenerator(r, o, cm, ownerInfo) - } - } - configMaps[constants.CapabilitiesGeneratorName] = func(r R, o O, cm *corev1.ConfigMap) error { - return mutateConfigMapCapabilitiesGenerator(r, o, cm, ownerInfo) - } - if obj.Options().PrefetchData { - configMaps[constants.InitScriptsName] = mutateConfigMapBlobDownload - } - if obj.Type() == pdoknlv3.ServiceTypeWMS { - wms, _ := any(obj).(*pdoknlv3.WMS) - wmsReconciler := (*WMSReconciler)(r) - - configMaps[constants.LegendGeneratorName] = func(_ R, _ O, cm *corev1.ConfigMap) error { - return mutateConfigMapLegendGenerator(wmsReconciler, wms, cm) - } - configMaps[constants.FeatureinfoGeneratorName] = func(_ R, _ O, cm *corev1.ConfigMap) error { - return mutateConfigMapFeatureinfoGenerator(wmsReconciler, wms, cm) - } - configMaps[constants.OgcWebserviceProxyName] = func(_ R, _ O, cm *corev1.ConfigMap) error { - return mutateConfigMapOgcWebserviceProxy(wmsReconciler, wms, cm) - } - } - for cmName, mutate := range configMaps { - cm, or, err := createOrUpdateConfigMap(ctx, obj, r, cmName, func(r R, o O, cm *corev1.ConfigMap) error { - return mutate(r, o, cm) - }) - if or != nil { - operationResults[smoothoperatorutils.GetObjectFullName(getReconcilerClient(r), cm)] = *or - } - if err != nil { - return hashedConfigMapNames, operationResults, err - } - switch cmName { - case constants.MapserverName: - hashedConfigMapNames.Mapserver = cm.Name - case constants.MapfileGeneratorName: - hashedConfigMapNames.MapfileGenerator = cm.Name - case constants.CapabilitiesGeneratorName: - hashedConfigMapNames.CapabilitiesGenerator = cm.Name - case constants.InitScriptsName: - hashedConfigMapNames.InitScripts = cm.Name - case constants.LegendGeneratorName: - hashedConfigMapNames.LegendGenerator = cm.Name - case constants.FeatureinfoGeneratorName: - hashedConfigMapNames.FeatureInfoGenerator = cm.Name - case constants.OgcWebserviceProxyName: - hashedConfigMapNames.OgcWebserviceProxy = cm.Name - } - } - - return hashedConfigMapNames, operationResults, err -} - -func createOrUpdateConfigMap[O pdoknlv3.WMSWFS, R Reconciler](ctx context.Context, obj O, reconciler R, name string, mutate func(R, O, *corev1.ConfigMap) error) (*corev1.ConfigMap, *controllerutil.OperationResult, error) { - reconcilerClient := getReconcilerClient(reconciler) - cm := getBareConfigMap(obj, name) - if err := mutate(reconciler, obj, cm); err != nil { - return cm, nil, err - } - or, err := controllerutil.CreateOrUpdate(ctx, reconcilerClient, cm, func() error { - return mutate(reconciler, obj, cm) - }) - if err != nil { - return cm, &or, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, cm), err) - } - return cm, &or, nil -} - -func createOrUpdateOrDeletePodDisruptionBudget[O pdoknlv3.WMSWFS, R Reconciler](ctx context.Context, reconciler R, obj O, operationResults map[string]controllerutil.OperationResult) (err error) { - reconcilerClient := getReconcilerClient(reconciler) - podDisruptionBudget := getBarePodDisruptionBudget(obj) - autoscalerPatch := obj.HorizontalPodAutoscalerPatch() - if autoscalerPatch != nil && autoscalerPatch.MinReplicas != nil && autoscalerPatch.MaxReplicas != nil && - *autoscalerPatch.MinReplicas == 1 && *autoscalerPatch.MaxReplicas == 1 { - err = reconcilerClient.Delete(ctx, podDisruptionBudget) - if err == nil { - operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget)] = "deleted" - } - if client.IgnoreNotFound(err) != nil { - return fmt.Errorf("unable to delete resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget), err) - } - } else { - operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, podDisruptionBudget, func() error { - return mutatePodDisruptionBudget(reconciler, obj, podDisruptionBudget) - }) - if err != nil { - return fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget), err) - } - } - return nil -} - -func recoveredPanicToError(rec any) (err error) { - switch x := rec.(type) { - case string: - err = errors.New(x) - case error: - err = x - default: - err = errors.New("unknown panic") - } - - // Add stack - // TODO - this doesn't seem to work, see if there is a better method to add the stack - err = errors.WithStack(err) - - return -} diff --git a/internal/controller/shared_controller_test.go b/internal/controller/shared_controller_test.go deleted file mode 100644 index 2d32671..0000000 --- a/internal/controller/shared_controller_test.go +++ /dev/null @@ -1,456 +0,0 @@ -package controller - -import ( - "context" - "fmt" - "os" - "slices" - "strings" - "testing" - - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - "github.com/stretchr/testify/assert" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - "github.com/google/go-cmp/cmp" - "github.com/pdok/mapserver-operator/api/v2beta1" - "github.com/pdok/mapserver-operator/internal/controller/types" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" - traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" - v2 "k8s.io/api/autoscaling/v2" - policyv1 "k8s.io/api/policy/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/yaml" - - . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd - . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - k8stypes "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - namespace = "default" - testImageName1 = "test.test/image:test1" - testImageName2 = "test.test/image:test2" - testImageName3 = "test.test/image:test3" - testImageName4 = "test.test/image:test4" - testImageName5 = "test.test/image:test5" - testImageName6 = "test.test/image:test6" - testImageName7 = "test.test/image:test7" -) - -func getHashedConfigMapNameFromClient[O pdoknlv3.WMSWFS](ctx context.Context, obj O, volumeName string) (string, error) { - deployment := &appsv1.Deployment{} - err := k8sClient.Get(ctx, k8stypes.NamespacedName{Namespace: obj.GetNamespace(), Name: getBareDeployment(obj).GetName()}, deployment) - if err != nil { - return "", err - } - - for _, volume := range deployment.Spec.Template.Spec.Volumes { - if volume.Name == volumeName && volume.ConfigMap != nil { - return volume.ConfigMap.Name, nil - } - } - return "", fmt.Errorf("configmap %s not found", volumeName) -} - -func getExpectedObjects[O pdoknlv3.WMSWFS](ctx context.Context, obj O, includeBlobDownload bool, includeMapfileGeneratorConfigMap bool) ([]client.Object, error) { - objects := []client.Object{ - getBareDeployment(obj), - getBareHorizontalPodAutoScaler(obj), - getBareService(obj), - getBareIngressRoute(obj), - getBareCorsHeadersMiddleware(obj), - getBarePodDisruptionBudget(obj), - } - - // Add all ConfigMaps with hashed names - cm := getBareConfigMap(obj, constants.MapserverName) - hashedName, err := getHashedConfigMapNameFromClient(ctx, obj, constants.MapserverName) - if err != nil { - return objects, err - } - cm.Name = hashedName - objects = append(objects, cm) - - if includeMapfileGeneratorConfigMap { - cm = getBareConfigMap(obj, constants.MapfileGeneratorName) - hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapMapfileGeneratorVolumeName) - if err != nil { - return objects, err - } - cm.Name = hashedName - objects = append(objects, cm) - } - - cm = getBareConfigMap(obj, constants.CapabilitiesGeneratorName) - hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapCapabilitiesGeneratorVolumeName) - if err != nil { - return objects, err - } - cm.Name = hashedName - objects = append(objects, cm) - - if includeBlobDownload { - cm = getBareConfigMap(obj, constants.InitScriptsName) - hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.InitScriptsName) - if err != nil { - return objects, err - } - cm.Name = hashedName - objects = append(objects, cm) - } - - if obj.Type() == pdoknlv3.ServiceTypeWMS { - wms, _ := any(obj).(*pdoknlv3.WMS) - cm = getBareConfigMap(wms, constants.LegendGeneratorName) - hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapLegendGeneratorVolumeName) - if err != nil { - return objects, err - } - cm.Name = hashedName - objects = append(objects, cm) - - cm = getBareConfigMap(wms, constants.FeatureinfoGeneratorName) - hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapFeatureinfoGeneratorVolumeName) - if err != nil { - return objects, err - } - cm.Name = hashedName - objects = append(objects, cm) - - if obj.Options().UseWebserviceProxy() { - cm = getBareConfigMap(wms, constants.OgcWebserviceProxyName) - hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapOgcWebserviceProxyVolumeName) - if err != nil { - return objects, err - } - cm.Name = hashedName - objects = append(objects, cm) - } - } - - return objects, nil -} - -func testPath(t pdoknlv3.ServiceType, test string) string { - return fmt.Sprintf("test_data/%s/%s/", strings.ToLower(string(t)), test) -} - -func testMutate[T any](kind string, result *T, expectedFile string, mutate func(*T) error) { - By("Testing mutating the " + kind) - err := mutate(result) - Expect(err).NotTo(HaveOccurred()) - - var expected T - data, err := os.ReadFile(expectedFile) - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, &expected) - Expect(err).NotTo(HaveOccurred()) - - diff := cmp.Diff(expected, *result) - if diff != "" { - Fail(diff) - } - - By(fmt.Sprintf("Testing mutating the %s twice has the same result", kind)) - generated := *result - err = mutate(result) - Expect(err).NotTo(HaveOccurred()) - diff = cmp.Diff(generated, *result) - if diff != "" { - Fail(diff) - } -} - -//nolint:unparam -func testMutateConfigMap(m *corev1.ConfigMap, expectedFile string, mutate func(*corev1.ConfigMap) error, ignoreValues bool) { - clearConfigMapValues := func(cm *corev1.ConfigMap) { - newMap := map[string]string{} - for k := range cm.Data { - newMap[k] = "IGNORED" - } - cm.Data = newMap - } - - if !ignoreValues { - testMutate("ConfigMap", m, expectedFile, mutate) - } else { - By("Testing mutating the ConfigMap") - err := mutate(m) - Expect(err).NotTo(HaveOccurred()) - - expected := &corev1.ConfigMap{} - data, err := os.ReadFile(expectedFile) - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, expected) - Expect(err).NotTo(HaveOccurred()) - - c := m.DeepCopy() - clearConfigMapValues(c) - clearConfigMapValues(expected) - - diff := cmp.Diff(*expected, *c) - if diff != "" { - Fail(diff) - } - } -} - -func testMutates[R Reconciler, O pdoknlv3.WMSWFS](reconcilerFn func() R, resource O, name string, ignoreFiles ...string) { - inputPath := testPath(resource.Type(), name) + "input/" - outputPath := testPath(resource.Type(), name) + "expected/" - - shouldIncludeFile := func(name string) (string, bool) { - if slices.Contains(ignoreFiles, name) { - return "", false - } - - return outputPath + name, true - } - - var fileName string - switch resource.Type() { - case pdoknlv3.ServiceTypeWFS: - fileName = "wfs.yaml" - case pdoknlv3.ServiceTypeWMS: - fileName = "wms.yaml" - default: - panic("unknown servicetype") - } - - owner := smoothoperatorv1.OwnerInfo{} - - It("Should parse the input files correctly", func() { - data, err := readTestFile(inputPath + fileName) - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, &resource) - Expect(err).NotTo(HaveOccurred()) - Expect(resource.GetName()).Should(Equal(name)) - - data, err = os.ReadFile(inputPath + "ownerinfo.yaml") - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, &owner) - Expect(err).NotTo(HaveOccurred()) - Expect(owner.Name).Should(Equal("owner")) - - Expect(k8sClient.Create(ctx, &owner)).To(Succeed()) - - var validationError error - switch any(resource).(type) { - case *pdoknlv3.WMS: - wms := any(resource).(*pdoknlv3.WMS) - _, validationError = wms.ValidateCreate(k8sClient) - case *pdoknlv3.WFS: - wfs := any(resource).(*pdoknlv3.WFS) - _, validationError = wfs.ValidateCreate(k8sClient) - } - Expect(validationError).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, &owner)).To(Succeed()) - }) - - configMapNames := types.HashedConfigMapNames{} - - It("Should generate a correct Configmap", func() { - cm := getBareConfigMap(resource, constants.MapserverName) - testMutateConfigMap(cm, outputPath+"configmap-mapserver.yaml", func(cm *corev1.ConfigMap) error { - return mutateConfigMap(reconcilerFn(), resource, cm) - }, true) - configMapNames.Mapserver = cm.Name - }) - - It("Should generate a correct BlobDownload Configmap", func() { - if path, include := shouldIncludeFile("configmap-init-scripts.yaml"); include { - cm := getBareConfigMap(resource, constants.InitScriptsName) - testMutateConfigMap(cm, path, func(cm *corev1.ConfigMap) error { - return mutateConfigMapBlobDownload(reconcilerFn(), resource, cm) - }, true) - configMapNames.InitScripts = cm.Name - } - }) - - It("Should generate a correct MapfileGenerator Configmap", func() { - if path, include := shouldIncludeFile("configmap-mapfile-generator.yaml"); include { - cm := getBareConfigMap(resource, constants.MapfileGeneratorName) - testMutateConfigMap(cm, path, func(cm *corev1.ConfigMap) error { - return mutateConfigMapMapfileGenerator(reconcilerFn(), resource, cm, &owner) - }, true) - configMapNames.MapfileGenerator = cm.Name - } - }) - - It("Should generate a correct CapabilitiesGenerator Configmap", func() { - cm := getBareConfigMap(resource, constants.CapabilitiesGeneratorName) - testMutateConfigMap(cm, outputPath+"configmap-capabilities-generator.yaml", func(cm *corev1.ConfigMap) error { - return mutateConfigMapCapabilitiesGenerator(reconcilerFn(), resource, cm, &owner) - }, true) - configMapNames.CapabilitiesGenerator = cm.Name - }) - - if resource.Type() == pdoknlv3.ServiceTypeWMS { - wms := any(resource).(*pdoknlv3.WMS) - It("Should generate a correct FeatureInfo Configmap", func() { - cm := getBareConfigMap(resource, constants.FeatureinfoGeneratorName) - testMutateConfigMap(cm, outputPath+"configmap-featureinfo-generator.yaml", func(cm *corev1.ConfigMap) error { - return mutateConfigMapFeatureinfoGenerator(getWMSReconciler(), wms, cm) - }, true) - configMapNames.FeatureInfoGenerator = cm.Name - }) - - It("Should generate a correct LegendGenerator Configmap", func() { - cm := getBareConfigMap(resource, constants.LegendGeneratorName) - testMutateConfigMap(cm, outputPath+"configmap-legend-generator.yaml", func(cm *corev1.ConfigMap) error { - return mutateConfigMapLegendGenerator(getWMSReconciler(), wms, cm) - }, true) - configMapNames.LegendGenerator = cm.Name - }) - - It("Should generate a correct OGC webservice proxy Configmap", func() { - cm := getBareConfigMap(resource, constants.OgcWebserviceProxyName) - testMutateConfigMap(cm, outputPath+"configmap-ogc-webservice-proxy.yaml", func(cm *corev1.ConfigMap) error { - return mutateConfigMapOgcWebserviceProxy(getWMSReconciler(), wms, cm) - }, true) - configMapNames.OgcWebserviceProxy = cm.Name - }) - } - - It("Should generate a Deployment correctly", func() { - testMutate("Deployment", getBareDeployment(resource), outputPath+"deployment.yaml", func(d *appsv1.Deployment) error { - return mutateDeployment(reconcilerFn(), resource, d, configMapNames) - }) - }) - - It("Should generate a correct Service", func() { - testMutate("Service", getBareService(resource), outputPath+"service.yaml", func(s *corev1.Service) error { - return mutateService(reconcilerFn(), resource, s) - }) - }) - - It("Should generate a correct Headers Middleware", func() { - testMutate("Headers Middleware", getBareCorsHeadersMiddleware(resource), outputPath+"middleware-headers.yaml", func(m *traefikiov1alpha1.Middleware) error { - return mutateCorsHeadersMiddleware(reconcilerFn(), resource, m) - }) - }) - - It("Should generate a correct IngressRoute", func() { - testMutate("IngressRoute", getBareIngressRoute(resource), outputPath+"ingressroute.yaml", func(i *traefikiov1alpha1.IngressRoute) error { - return mutateIngressRoute(reconcilerFn(), resource, i) - }) - }) - - It("Should generate a correct PodDisruptionBudget", func() { - testMutate("PodDisruptionBudget", getBarePodDisruptionBudget(resource), outputPath+"poddisruptionbudget.yaml", func(p *policyv1.PodDisruptionBudget) error { - return mutatePodDisruptionBudget(reconcilerFn(), resource, p) - }) - }) - - It("Should generate a correct HorizontalPodAutoscaler", func() { - testMutate("PodDisruptionBudget", getBareHorizontalPodAutoScaler(resource), outputPath+"horizontalpodautoscaler.yaml", func(h *v2.HorizontalPodAutoscaler) error { - return mutateHorizontalPodAutoscaler(reconcilerFn(), resource, h) - }) - }) -} - -func readTestFile(fileName string) ([]byte, error) { - dat, err := os.ReadFile(fileName) - if err != nil { - return []byte{}, err - } - - // Temporary check if the input file is a v2, if so, convert to v3 - dat, err = convertAndWriteIfWMSWFS(dat, fileName) - if err != nil { - return []byte{}, err - } - - // Apply defaults - un := unstructured.Unstructured{} - err = yaml.Unmarshal(dat, &un) - if slices.Contains([]string{"WMS", "WFS"}, un.GetKind()) { - defaulted, err := smoothoperatorvalidation.ApplySchemaDefaults(un.Object) - if err != nil { - return []byte{}, err - } - - return yaml.Marshal(defaulted) - } - - return dat, err -} - -func convertAndWriteIfWMSWFS(data []byte, fileName string) ([]byte, error) { - un := unstructured.Unstructured{} - err := yaml.Unmarshal(data, &un) - if err != nil { - return []byte{}, err - } - - if un.GetAPIVersion() == "pdok.nl/v2beta1" { - switch un.GetKind() { - case "WFS": - v2Wfs := v2beta1.WFS{} - err = yaml.UnmarshalStrict(data, &v2Wfs) - if err != nil { - return []byte{}, err - } - v3 := pdoknlv3.WFS{} - err = v2Wfs.ToV3(&v3) - if err != nil { - return []byte{}, err - } - data, err = yaml.Marshal(v3) - case "WMS": - v2Wms := v2beta1.WMS{} - err = yaml.UnmarshalStrict(data, &v2Wms) - if err != nil { - return []byte{}, err - } - v3 := pdoknlv3.WMS{} - err = v2Wms.ToV3(&v3) - if err != nil { - return []byte{}, err - } - data, err = yaml.Marshal(v3) - } - - _ = os.WriteFile(fileName, data, 0644) - } - - return data, err -} - -func TestGetVolumesForDeployment(t *testing.T) { - wfs := &pdoknlv3.WFS{} - data, err := os.ReadFile(testPath("wfs", "minimal") + "input/wfs.yaml") - assert.NoError(t, err) - err = yaml.UnmarshalStrict(data, &wfs) - assert.NoError(t, err) - assert.Equal(t, wfs.Name, "minimal") - pdoknlv3.SetHost("https://service.pdok.nl") - - hashedConfigMapNames := types.HashedConfigMapNames{ - Mapserver: "rws-nwbwegen-v1-0-wfs-mapserver-bb59c7f4f4", - InitScripts: "2", - MapfileGenerator: "rws-nwbwegen-v1-0-wfs-mapfile-generator-bbbtd999dh", - CapabilitiesGenerator: "rws-nwbwegen-v1-0-wfs-capabilities-generator-6m4mfkgb5d", - OgcWebserviceProxy: "3", - LegendGenerator: "4", - FeatureInfoGenerator: "5", - } - result := getVolumes(wfs, hashedConfigMapNames) - - expected := []corev1.Volume{ - {Name: constants.BaseVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, - {Name: constants.DataVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, - {Name: constants.MapserverName, VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: "rws-nwbwegen-v1-0-wfs-mapserver-bb59c7f4f4"}, DefaultMode: smoothoperatorutils.Pointer(int32(420))}}}, - {Name: constants.ConfigMapCapabilitiesGeneratorVolumeName, VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: "rws-nwbwegen-v1-0-wfs-capabilities-generator-6m4mfkgb5d"}, DefaultMode: smoothoperatorutils.Pointer(int32(420))}}}, - {Name: constants.ConfigMapMapfileGeneratorVolumeName, VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: "rws-nwbwegen-v1-0-wfs-mapfile-generator-bbbtd999dh"}, DefaultMode: smoothoperatorutils.Pointer(int32(420))}}}, - } - - assert.Equal(t, expected, result) -} diff --git a/internal/controller/static/files/default_mapserver.conf b/internal/controller/static/files/default_mapserver.conf deleted file mode 100644 index 6e17bba..0000000 --- a/internal/controller/static/files/default_mapserver.conf +++ /dev/null @@ -1,8 +0,0 @@ -CONFIG - ENV - MS_MAP_NO_PATH "true" - END - MAPS - MAP "/srv/data/config/mapfile/service.map" - END -END \ No newline at end of file diff --git a/internal/controller/static/files/include.conf b/internal/controller/static/files/include.conf deleted file mode 100644 index 410d46d..0000000 --- a/internal/controller/static/files/include.conf +++ /dev/null @@ -1,15 +0,0 @@ -server.modules += ( "mod_status" ) - -$HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { - status.status-url = "/server-status" -} - -url.rewrite-once = ( -{{ rewrite_rules }} -) - -magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) - -setenv.add-environment += ( - "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, -) diff --git a/internal/controller/static/files/ogc.lua b/internal/controller/static/files/ogc.lua deleted file mode 100644 index 06e86e4..0000000 --- a/internal/controller/static/files/ogc.lua +++ /dev/null @@ -1,83 +0,0 @@ -if lighty.r.req_attr["request.method"] == "GET" then - - -- obtain service type from environment - serviceType = os.getenv('SERVICE_TYPE'):lower() - - path = lighty.r.req_attr["uri.path"] - query = lighty.r.req_attr["uri.query"] - - -- handle legend requests - if serviceType == "wms" then - _, _, file = path:find(".*/legend/(.*)") - if file then - if file:find(".*%.png") then - local legendPath = "/var/www/legend/" .. file - local stat = lighty.stat(legendPath) - if (not stat or not stat.is_file) then - -- don't serve non existing legend file - return 404 - end - lighty.content = { { filename = legendPath } } - lighty.header['Content-Type'] = "image/png" - return 200 - end - - return 404 - end - end - - params = {} - if query then - for k, v in query:gmatch("([^?&=]+)=([^&]+)") do - k = k:lower() - - params[k] = v - end - end - - -- assign service and version default values - version = params['version'] - service = params['service'] - - if not service then - service = serviceType - else - service = service:lower() - end - - if (service == 'wms' and (not version or version ~= '1.1.1')) then - version = '1.3.0' - end - - if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then - version = '2.0.0' - end - - -- serve static content - request = params['request'] - if request then - request = request:lower() - - staticStatus = 200 - staticContentType = 'text/xml; charset=UTF-8' - if request == 'getcapabilities' then - if (service == 'wms' and version == '1.3.0') then - staticFile = '/var/www/config/capabilities_wms_130.xml' - elseif (service == 'wfs' and version == '2.0.0') then - staticFile = '/var/www/config/capabilities_wfs_200.xml' - end - elseif service == 'wfs' and request == 'getfeature' then - startindex = params['startindex'] - if startindex and tonumber(startindex) > 50000 then - staticFile = '/srv/mapserver/config/scraping-error.xml' - staticStatus = 400 - end - end - - if staticFile then - lighty.content = { { filename = staticFile } } - lighty.header['Content-Type'] = staticContentType - return staticStatus - end - end -end \ No newline at end of file diff --git a/internal/controller/static/files/scraping-error.xml b/internal/controller/static/files/scraping-error.xml deleted file mode 100644 index 5632f19..0000000 --- a/internal/controller/static/files/scraping-error.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. - - - diff --git a/internal/controller/static/reader.go b/internal/controller/static/reader.go deleted file mode 100644 index 77a3cdb..0000000 --- a/internal/controller/static/reader.go +++ /dev/null @@ -1,26 +0,0 @@ -package static - -import ( - "embed" - "slices" -) - -//go:embed files -var embeddedFiles embed.FS - -func GetStaticFiles() ([]string, map[string][]byte) { - // Hardcoded order to get the same order as the old ansible operator - orderedNames := []string{"include.conf", "ogc.lua", "default_mapserver.conf", "scraping-error.xml"} - result := map[string][]byte{} - - files, _ := embeddedFiles.ReadDir("files") - for _, f := range files { - content, _ := embeddedFiles.ReadFile("files/" + f.Name()) - result[f.Name()] = content - if !slices.Contains(orderedNames, f.Name()) { - orderedNames = append(orderedNames, f.Name()) - } - } - - return orderedNames, result -} diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 717400e..468bc43 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -1,52 +1,31 @@ /* -MIT License - -Copyright (c) 2024 Publieke Dienstverlening op de Kaart - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package controller -//nolint:revive // Complains about the dot imports import ( "context" - "encoding/json" - "errors" "os" - "os/exec" "path/filepath" "testing" - pdoknlv2beta1 "github.com/pdok/mapserver-operator/api/v2beta1" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" - traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" - "golang.org/x/tools/go/packages" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" @@ -77,43 +56,18 @@ func TestControllers(t *testing.T) { var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - //nolint:fatcontext ctx, cancel = context.WithCancel(context.TODO()) - scheme := runtime.NewScheme() var err error - err = pdoknlv2beta1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = pdoknlv3.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = traefikiov1alpha1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = smoothoperatorv1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = clientgoscheme.AddToScheme(scheme) + err = pdoknlv3.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") - traefikCRDPath := must(getTraefikCRDPath()) - ownerInfoCRDPath := must(getOwnerInfoCRDPath()) testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, - CRDInstallOptions: envtest.CRDInstallOptions{ - Scheme: scheme, - Paths: []string{ - filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wfs.yaml"), - filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wms.yaml"), - traefikCRDPath, - ownerInfoCRDPath, - }, - ErrorIfPathMissing: true, - }, } // Retrieve the first found binary directory to allow running tests from IDEs @@ -126,56 +80,9 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) - - // Deploy blob configmap + secret - blobConfig := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "blobs-testtest", - Namespace: metav1.NamespaceDefault, - }, - } - err = k8sClient.Create(ctx, blobConfig) - Expect(err).NotTo(HaveOccurred()) - - blobSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "blobs-testtest", - Namespace: metav1.NamespaceDefault, - }, - } - err = k8sClient.Create(ctx, blobSecret) - Expect(err).NotTo(HaveOccurred()) - - // Deploy postgres configmap + secret - postgresConfig := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "postgres-testtest", - Namespace: metav1.NamespaceDefault, - }, - } - err = k8sClient.Create(ctx, postgresConfig) - Expect(err).NotTo(HaveOccurred()) - - postgresSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "postgres-testtest", - Namespace: metav1.NamespaceDefault, - }, - } - err = k8sClient.Create(ctx, postgresSecret) - Expect(err).NotTo(HaveOccurred()) - - // Load CRD schemas - err = smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wfs.pdok.nl") - Expect(err).NotTo(HaveOccurred()) - err = smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wms.pdok.nl") - Expect(err).NotTo(HaveOccurred()) - - pdoknlv3.SetHost("http://localhost:32788") - SetStorageClassName("test-storage") }) var _ = AfterSuite(func() { @@ -207,42 +114,3 @@ func getFirstFoundEnvTestBinaryDir() string { } return "" } - -func getOwnerInfoCRDPath() (string, error) { - smoothOperatorModule, err := getModule("github.com/pdok/smooth-operator") - if err != nil { - return "", err - } - if smoothOperatorModule.Dir == "" { - return "", errors.New("cannot find path for smooth-operator module") - } - return filepath.Join(smoothOperatorModule.Dir, "config", "crd", "bases", "pdok.nl_ownerinfo.yaml"), nil -} - -func getTraefikCRDPath() (string, error) { - traefikModule, err := getModule("github.com/traefik/traefik/v3") - if err != nil { - return "", err - } - if traefikModule.Dir == "" { - return "", errors.New("cannot find path for traefik module") - } - return filepath.Join(traefikModule.Dir, "integration", "fixtures", "k8s", "01-traefik-crd.yml"), nil -} - -func getModule(name string) (module *packages.Module, err error) { - out, err := exec.Command("go", "list", "-json", "-m", name).Output() - if err != nil { - return - } - module = &packages.Module{} - err = json.Unmarshal(out, module) - return -} - -func must[T any](t T, err error) T { - if err != nil { - panic(err) - } - return t -} diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index b29d959..0000000 --- a/internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: v1 -data: - input.yaml: |- - global: - additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd - namespace: http://dataset.geonovum.nl - onlineresourceurl: http://localhost - path: /datasetOwner/dataset/theme - prefix: dataset - version: v1_0 - services: - wfs200: - definition: - capabilities: - featuretypelist: - featuretype: - - abstract: feature "1" abstract - defaultcrs: urn:ogc:def:crs:EPSG::28992 - keywords: - - keyword: - - featuretype-1-keyword-1 - - featuretype-1-keyword-2 - metadataurl: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - name: dataset:featuretype-1-name - othercrs: - - urn:ogc:def:crs:EPSG::25831 - - urn:ogc:def:crs:EPSG::25832 - - urn:ogc:def:crs:EPSG::3034 - - urn:ogc:def:crs:EPSG::3035 - - urn:ogc:def:crs:EPSG::3857 - - urn:ogc:def:crs:EPSG::4258 - - urn:ogc:def:crs:EPSG::4326 - title: feature "1" title - - abstract: feature "2" abstract - defaultcrs: urn:ogc:def:crs:EPSG::28992 - keywords: - - keyword: - - featuretype-2-keyword-1 - - featuretype-2-keyword-2 - metadataurl: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - name: dataset:featuretype-2-name - othercrs: - - urn:ogc:def:crs:EPSG::25831 - - urn:ogc:def:crs:EPSG::25832 - - urn:ogc:def:crs:EPSG::3034 - - urn:ogc:def:crs:EPSG::3035 - - urn:ogc:def:crs:EPSG::3857 - - urn:ogc:def:crs:EPSG::4258 - - urn:ogc:def:crs:EPSG::4326 - title: feature "2" title - - abstract: featuretype-3-abstract - defaultcrs: urn:ogc:def:crs:EPSG::28992 - keywords: - - keyword: - - featuretype-3-keyword-1 - - featuretype-3-keyword-2 - metadataurl: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - name: dataset:featuretype-3-name - othercrs: - - urn:ogc:def:crs:EPSG::25831 - - urn:ogc:def:crs:EPSG::25832 - - urn:ogc:def:crs:EPSG::3034 - - urn:ogc:def:crs:EPSG::3035 - - urn:ogc:def:crs:EPSG::3857 - - urn:ogc:def:crs:EPSG::4258 - - urn:ogc:def:crs:EPSG::4326 - title: featuretype-3-title - operationsmetadata: - extendedcapabilities: - extendedcapabilities: - metadataurl: - mediatype: application/vnd.ogc.csw.GetRecordByIdResponse_xml - url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=metameta-meta-meta-meta-metametameta - responselanguage: - language: dut - spatialdatasetidentifier: - code: bronbron-bron-bron-bron-bronbronbron - supportedlanguages: - defaultlanguage: - language: dut - serviceidentification: - abstract: some "Service" abstract - accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - keywords: - keyword: - - service-keyword-1 - - service-keyword-2 - - infoFeatureAccessService - title: some Service title - serviceprovider: - providersite: - href: http://localhost - type: simple - filename: /var/www/config/capabilities_wfs_200.xml - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-capabilities-generator-mfbh8cgh5c - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml deleted file mode 100644 index 2a6d546..0000000 --- a/internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml +++ /dev/null @@ -1,190 +0,0 @@ -apiVersion: v1 -data: - gpkg_download.sh: |- - #!/usr/bin/env bash - - set -euo pipefail - - function download_gpkg() { - local gpkg=$1 - local file=$2 - local url=$3 - - if [ -f "$file" ] && [ ! -f "$file".st ]; then - echo msg=\"File already downloaded\" file=\""$file"\" - else - echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" - - # use curl to check if resource exists - # axel blocks on non-existing resources - curl -IfsS "$url" > /dev/null - - echo start "$gpkg" - ret=0 - # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. - axel -n 1 -T 120 -o "$file" "$url" \ - | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ - | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? - - if [ $ret -ne 0 ] - then - echo -e '\n' - # Download failed ($? != 0). - if [ $ret -eq 1 ] - then - # Axel was not able to resume ($? == 1). Remove file and state file. - if [ -f "$file" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file"\" - rm "$file" - fi - if [ -f "$file.st" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file".st\" - rm "$file".st - fi - else - # Download failed with other error ($? > 1). Remove file if state file does not exist. - if [ ! -f "$file.st" ]; then - echo msg=\"Download failed without state file, removing file\" file=\""$file"\" - rm "$file" - fi - fi - - # Retry the download - echo msg=\"Retry file\" file=\""$file"\" - download_gpkg $gpkg $file $url - fi - fi - } - - function download() { - if [ -z "$BLOBS_ENDPOINT" ]; - then - echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; - exit 1; - fi - - local gpkg=$1 - local file=/srv/data/gpkg/$2 - local url=${BLOBS_ENDPOINT}/${gpkg} - - download_gpkg $gpkg $file $url - - # Check Content-length - download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') - file_size=$(wc -c "$file" | awk '{print $1}') - if [ "$download_size" != "$file_size" ] - then - echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - rm_file_and_exit - else - echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - chown 999:999 "$file" - fi - - # Check ogrinfo - echo "Check gpkg with ogrinfo" - if ! ogrinfo -so "$file" - then - echo "ERROR: ogrinfo check on $file failed" - rm_file_and_exit - fi - - # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) - echo "Check if md5 hash value exists in blob storage" - rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" - - # If file contains valid hash, then check it, else skip - hash=$(awk '{ print $1 }' "${file}.md5sum-remote") - if [[ $hash =~ ^[a-f0-9]{32}$ ]] - then - echo "Valid hash value found" - echo "Compare MD5 hash of remote and downloaded gpkg" - if ! (echo "$hash $file" | md5sum --check); then - rm_file_and_exit - fi - else - echo "No hash found for $file in blob storage, skipping checksum." - fi - - echo "done" - } - - function download_all() { - echo msg=\"Starting GeoPackage downloader\" - - local start_time=$(date '+%s') - - # create target location if not exists - mkdir -p /srv/data/gpkg - chown 999:999 /srv/data/gpkg - - download ${BLOBS_GEOPACKAGES_BUCKET}/key/file-1.gpkg file-1.gpkg; - download ${BLOBS_GEOPACKAGES_BUCKET}/key/file-2.gpkg file-2.gpkg; - - echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) - } - - function rm_file_and_exit() { - echo "Removing $file, to ensure a fresh new download is started when script is executed again" - rm -rf "$file" - - if [ -f "$file.st" ]; then - rm "$file".st - fi - - echo "Exiting..." - exit 1 - } - - download_all | awk -W interactive ' - BEGIN { - state="idle"; - } - - { - if ($0 != "") { - if ($1 == "start") { - gpkg=$2; - state="downloading"; - } else if ($1 == "done") { - state="idle"; - } else if (state == "downloading") { - if ($1 == "progress") { - # reduce output to prevent loki from choking on large log volume - if (last_percentage != $2) { - if ($3 == "") { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; - } else { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; - } - } - last_percentage=$2; - } else { - print "msg=\"" $0 "\" gpkg=" gpkg; - } - } else { - print $0; - } - } - } - ' -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-init-scripts-f8k8ffgmgh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml deleted file mode 100644 index 1b67321..0000000 --- a/internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml +++ /dev/null @@ -1,122 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "service_title": "some Service title", - "service_abstract": "some \"Service\" abstract", - "service_keywords": "service-keyword-1,service-keyword-2,infoFeatureAccessService", - "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "service-extent", - "service_wfs_maxfeatures": "1000", - "service_namespace_prefix": "dataset", - "service_namespace_uri": "http://dataset.geonovum.nl", - "service_onlineresource": "http://localhost", - "service_path": "/datasetOwner/dataset/theme/wfs/v1_0", - "service_metadata_id": "metameta-meta-meta-meta-metametameta", - "dataset_owner": "authority", - "authority_url": "https://authority-url", - "automatic_casing": true, - "data_epsg": "EPSG:28992", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326" - ], - "layers": [ - { - "name": "featuretype-1-name", - "title": "feature \"1\" title", - "abstract": "feature \"1\" abstract", - "keywords": "featuretype-1-keyword-1,featuretype-1-keyword-2", - "layer_extent": "featuretype-1-extent", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "featuretype-1-column-1" - }, - { - "name": "featuretype-1-column-2" - } - ], - "geometry_type": "Point", - "gpkg_path": "/srv/data/gpkg/file-1.gpkg", - "tablename": "featuretype-1" - }, - { - "name": "featuretype-2-name", - "title": "feature \"2\" title", - "abstract": "feature \"2\" abstract", - "keywords": "featuretype-2-keyword-1,featuretype-2-keyword-2", - "layer_extent": "featuretype-2-extent", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "featuretype-2-column-1", - "alias": "ALIAS_featuretype-2-column-1" - }, - { - "name": "featuretype-2-column-2" - } - ], - "geometry_type": "MultiLine", - "gpkg_path": "/srv/data/gpkg/file-2.gpkg", - "tablename": "featuretype-2" - }, - { - "name": "featuretype-3-name", - "title": "featuretype-3-title", - "abstract": "featuretype-3-abstract", - "keywords": "featuretype-3-keyword-1,featuretype-3-keyword-2", - "layer_extent": "featuretype-3-extent", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "tablename": "featuretype-3", - "geometry_type": "MultiLine", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "featuretype-3-column-1" - }, - { - "name": "featuretype-3-column-2" - } - ], - "postgis": true - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapfile-generator-dkmmf7b5hf - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml deleted file mode 100644 index 6f04c9c..0000000 --- a/internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,143 +0,0 @@ -apiVersion: v1 -data: - default_mapserver.conf: |- - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - MAPS - MAP "/srv/data/config/mapfile/service.map" - END - END - include.conf: | - server.modules += ( "mod_status" ) - - $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { - status.status-url = "/server-status" - } - - url.rewrite-once = ( - "/datasetOwner/dataset/theme/wfs/v1_0/legend(.*)" => "/legend$1", - "/datasetOwner/dataset/theme/wfs/v1_0(.*)" => "/mapserver$1", - "/other/path/legend(.*)" => "/legend$1", - "/other/path(.*)" => "/mapserver$1" - ) - - magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) - - setenv.add-environment += ( - "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, - ) - ogc.lua: |- - if lighty.r.req_attr["request.method"] == "GET" then - - -- obtain service type from environment - serviceType = os.getenv('SERVICE_TYPE'):lower() - - path = lighty.r.req_attr["uri.path"] - query = lighty.r.req_attr["uri.query"] - - -- handle legend requests - if serviceType == "wms" then - _, _, file = path:find(".*/legend/(.*)") - if file then - if file:find(".*%.png") then - local legendPath = "/var/www/legend/" .. file - local stat = lighty.stat(legendPath) - if (not stat or not stat.is_file) then - -- don't serve non existing legend file - return 404 - end - lighty.content = { { filename = legendPath } } - lighty.header['Content-Type'] = "image/png" - return 200 - end - - return 404 - end - end - - params = {} - if query then - for k, v in query:gmatch("([^?&=]+)=([^&]+)") do - k = k:lower() - - params[k] = v - end - end - - -- assign service and version default values - version = params['version'] - service = params['service'] - - if not service then - service = serviceType - else - service = service:lower() - end - - if (service == 'wms' and (not version or version ~= '1.1.1')) then - version = '1.3.0' - end - - if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then - version = '2.0.0' - end - - -- serve static content - request = params['request'] - if request then - request = request:lower() - - staticStatus = 200 - staticContentType = 'text/xml; charset=UTF-8' - if request == 'getcapabilities' then - if (service == 'wms' and version == '1.3.0') then - staticFile = '/var/www/config/capabilities_wms_130.xml' - elseif (service == 'wfs' and version == '2.0.0') then - staticFile = '/var/www/config/capabilities_wfs_200.xml' - end - elseif service == 'wfs' and request == 'getfeature' then - startindex = params['startindex'] - if startindex and tonumber(startindex) > 50000 then - staticFile = '/srv/mapserver/config/scraping-error.xml' - staticStatus = 400 - end - end - - if staticFile then - lighty.content = { { filename = staticFile } } - lighty.header['Content-Type'] = staticContentType - return staticStatus - end - end - end - scraping-error.xml: | - - - - - It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. - - - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapserver-bfcm4d47kh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/deployment.yaml b/internal/controller/test_data/wfs/complete/expected/deployment.yaml deleted file mode 100644 index 8f6d85f..0000000 --- a/internal/controller/test_data/wfs/complete/expected/deployment.yaml +++ /dev/null @@ -1,270 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: true - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: '9117' - prometheus.io/scrape: true - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - spec: - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - - name: SERVICE_TYPE - value: WFS - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - terminationMessagePolicy: File - terminationMessagePath: /dev/termination-log - lifecycle: - preStop: - exec: - command: - - sleep - - '15' - livenessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - ports: - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?Service=WFS&Request=GetCapabilities'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/html''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - resources: - limits: - cpu: '2' - memory: 500M - requests: - cpu: '1' - memory: 250M - startupProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?Service=WFS&Request=GetCapabilities'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/html''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - args: - - --scrape_uri=http://localhost/server-status?auto - image: test.test/image:test5 - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePolicy: File - terminationMessagePath: /dev/termination-log - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: '0.02' - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - bash /srv/scripts/gpkg_download.sh; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file-1.gpkg;${BLOBS_GEOPACKAGES_BUCKET}/key/file-2.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePolicy: File - terminationMessagePath: /dev/termination-log - resources: - requests: - cpu: '0.15' - limits: - cpu: '1' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - mountPath: /srv/scripts - name: init-scripts - readOnly: true - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:test4 - imagePullPolicy: IfNotPresent - name: capabilities-generator - terminationMessagePolicy: File - terminationMessagePath: /dev/termination-log - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --not-include - - wfs - - /input/input.json - - /srv/data/config/mapfile - command: - - generate-mapfile - envFrom: - - configMapRef: - name: postgres-testtest - - secretRef: - name: postgres-testtest - image: test.test/image:test2 - imagePullPolicy: IfNotPresent - name: mapfile-generator - terminationMessagePolicy: File - terminationMessagePath: /dev/termination-log - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: mapfile-generator-config - readOnly: true - terminationGracePeriodSeconds: 60 - restartPolicy: Always - dnsPolicy: ClusterFirst - volumes: - - ephemeral: - volumeClaimTemplate: - spec: - storageClassName: test-storage - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 11G - name: base - - emptyDir: {} - name: data - - configMap: - name: complete-wfs-mapserver-bfcm4d47kh - defaultMode: 420 - name: mapserver - - configMap: - defaultMode: 511 - name: complete-wfs-init-scripts-f8k8ffgmgh - name: init-scripts - - configMap: - name: complete-wfs-capabilities-generator-mfbh8cgh5c - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: complete-wfs-mapfile-generator-dkmmf7b5hf - defaultMode: 420 - name: mapfile-generator-config diff --git a/internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index 614eb9f..0000000 --- a/internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 600 - type: Percent - value: 10 - - periodSeconds: 600 - type: Pods - value: 1 - selectPolicy: Max - stabilizationWindowSeconds: 3600 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 300 - maxReplicas: 50 - metrics: - - resource: - name: cpu - target: - averageUtilization: 20 - type: Utilization - type: Resource - minReplicas: 1 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: complete-wfs-mapserver diff --git a/internal/controller/test_data/wfs/complete/expected/ingressroute.yaml b/internal/controller/test_data/wfs/complete/expected/ingressroute.yaml deleted file mode 100644 index 6e9192d..0000000 --- a/internal/controller/test_data/wfs/complete/expected/ingressroute.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true - annotations: - uptime.pdok.nl/id: fbe1241d4fed04fb85d2135f182427861156f692 - uptime.pdok.nl/name: COMPLETE INSPIRE WFS - uptime.pdok.nl/tags: public-stats,wfs,inspire - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/theme/wfs/v1_0?Service=WFS&Request=GetCapabilities -spec: - routes: - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/theme/wfs/v1_0`) - middlewares: - - name: complete-wfs-mapserver-headers - services: - - kind: Service - name: complete-wfs-mapserver - port: 80 - - kind: Rule - match: Host(`localhost`) && Path(`/other/path`) - middlewares: - - name: complete-wfs-mapserver-headers - services: - - kind: Service - name: complete-wfs-mapserver - port: 80 diff --git a/internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml b/internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml deleted file mode 100644 index 46e387c..0000000 --- a/internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml deleted file mode 100644 index 7a2f186..0000000 --- a/internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme diff --git a/internal/controller/test_data/wfs/complete/expected/service.yaml b/internal/controller/test_data/wfs/complete/expected/service.yaml deleted file mode 100644 index 6a8ac84..0000000 --- a/internal/controller/test_data/wfs/complete/expected/service.yaml +++ /dev/null @@ -1,41 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme - name: complete-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - type: ClusterIP - sessionAffinity: None - internalTrafficPolicy: Cluster - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: true - service-type: wfs - service-version: v1_0 - theme: theme diff --git a/internal/controller/test_data/wfs/complete/input/ownerinfo.yaml b/internal/controller/test_data/wfs/complete/input/ownerinfo.yaml deleted file mode 100644 index 75a212e..0000000 --- a/internal/controller/test_data/wfs/complete/input/ownerinfo.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wfs: - serviceProvider: - providerName: PDOK diff --git a/internal/controller/test_data/wfs/complete/input/wfs.yaml b/internal/controller/test_data/wfs/complete/input/wfs.yaml deleted file mode 100644 index f9d8a29..0000000 --- a/internal/controller/test_data/wfs/complete/input/wfs.yaml +++ /dev/null @@ -1,166 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WFS -metadata: - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wfs - service-version: v1_0 - theme: theme - name: complete - namespace: default -spec: - ingressRouteUrls: - - url: http://localhost:32788/datasetOwner/dataset/theme/wfs/v1_0 - - url: http://localhost:32788/other/path - healthCheck: - querystring: Service=WFS&Request=GetCapabilities - mimetype: text/html - horizontalPodAutoscalerPatch: - maxReplicas: 50 - metrics: - - resource: - name: cpu - target: - averageUtilization: 20 - type: Utilization - type: Resource - minReplicas: 1 - options: {} - podSpecPatch: - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - - name: mapfile-generator - envFrom: - - configMapRef: - name: postgres-testtest - - secretRef: - name: postgres-testtest - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - resources: - limits: - cpu: "2" - ephemeral-storage: 11G - memory: 500M - requests: - cpu: "1" - ephemeral-storage: 11G - memory: 250M - service: - abstract: some "Service" abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - bbox: - defaultCRS: - maxx: "3" - maxy: "4" - minx: "1" - miny: "2" - defaultCrs: EPSG:28992 - featureTypes: - - abstract: feature "1" abstract - bbox: - defaultCRS: - maxx: "3" - maxy: "4" - minx: "1" - miny: "2" - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file-1.gpkg - columns: - - name: featuretype-1-column-1 - - name: featuretype-1-column-2 - geometryType: Point - tableName: featuretype-1 - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - featuretype-1-keyword-1 - - featuretype-1-keyword-2 - name: featuretype-1-name - title: feature "1" title - - abstract: feature "2" abstract - bbox: - defaultCRS: - maxx: "7" - maxy: "8" - minx: "5" - miny: "6" - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file-2.gpkg - columns: - - alias: ALIAS_featuretype-2-column-1 - name: featuretype-2-column-1 - - name: featuretype-2-column-2 - geometryType: MultiLineString - tableName: featuretype-2 - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - featuretype-2-keyword-1 - - featuretype-2-keyword-2 - name: featuretype-2-name - title: feature "2" title - - abstract: featuretype-3-abstract - bbox: - defaultCRS: - maxx: "9" - maxy: "0" - minx: "1" - miny: "2" - wgs84: - maxx: "180" - maxy: "90" - minx: "-180" - miny: "-90" - data: - postgis: - columns: - - name: featuretype-3-column-1 - - name: featuretype-3-column-2 - geometryType: MultiLineString - tableName: featuretype-3 - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - featuretype-3-keyword-1 - - featuretype-3-keyword-2 - name: featuretype-3-name - title: featuretype-3-title - inspire: - language: dut - serviceMetadataUrl: - csw: - metadataIdentifier: metameta-meta-meta-meta-metametameta - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - keywords: - - service-keyword-1 - - service-keyword-2 - otherCrs: - - EPSG:25831 - - EPSG:25832 - - EPSG:3034 - - EPSG:3035 - - EPSG:3857 - - EPSG:4258 - - EPSG:4326 - ownerInfoRef: owner - prefix: dataset - title: some Service title - url: http://localhost:32788/datasetOwner/dataset/theme/wfs/v1_0 diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index 76bb089..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -data: - input.yaml: "..." -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: minimal-wfs-capabilities-generator-m46924mtk7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml deleted file mode 100644 index 367a3f0..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -data: - gpkg_download.sh: "..." -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: minimal-wfs-init-scripts-f8k8ffgmgh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml deleted file mode 100644 index 9bc7ba5..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -data: - input.json: "..." -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapfile-generator-cdchdd74m7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml deleted file mode 100644 index 3682d6c..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -apiVersion: v1 -data: - default_mapserver.conf: "..." - include.conf: "..." - ogc.lua: "..." - scraping-error.xml: "..." -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapserver-f5ch9b2bhh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/deployment.yaml b/internal/controller/test_data/wfs/minimal/expected/deployment.yaml deleted file mode 100644 index 9001517..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/deployment.yaml +++ /dev/null @@ -1,253 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: '9117' - prometheus.io/scrape: 'true' - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - spec: - containers: - - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - - name: SERVICE_TYPE - value: WFS - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - lifecycle: - preStop: - exec: - command: - - sleep - - '15' - livenessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - name: mapserver - ports: - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - resources: - limits: - ephemeral-storage: 200M - memory: 800M - requests: - cpu: '0.15' - startupProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - args: - - --scrape_uri=http://localhost/server-status?auto - image: test.test/image:test5 - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: '0.02' - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - bash /srv/scripts/gpkg_download.sh; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - requests: - cpu: '0.15' - limits: - cpu: '0.2' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - mountPath: /srv/scripts - name: init-scripts - readOnly: true - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:test4 - imagePullPolicy: IfNotPresent - name: capabilities-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --not-include - - wfs - - /input/input.json - - /srv/data/config/mapfile - command: - - generate-mapfile - image: test.test/image:test2 - imagePullPolicy: IfNotPresent - name: mapfile-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: mapfile-generator-config - readOnly: true - restartPolicy: Always - terminationGracePeriodSeconds: 60 - dnsPolicy: ClusterFirst - volumes: - - emptyDir: {} - name: base - - emptyDir: {} - name: data - - configMap: - name: minimal-wfs-mapserver-f5ch9b2bhh - defaultMode: 420 - name: mapserver - - configMap: - defaultMode: 511 - name: minimal-wfs-init-scripts-f8k8ffgmgh - name: init-scripts - - configMap: - name: minimal-wfs-capabilities-generator-m46924mtk7 - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: minimal-wfs-mapfile-generator-cdchdd74m7 - defaultMode: 420 - name: mapfile-generator-config diff --git a/internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index c35c146..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,52 +0,0 @@ ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 600 - type: Percent - value: 10 - - periodSeconds: 600 - type: Pods - value: 1 - selectPolicy: Max - stabilizationWindowSeconds: 3600 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 300 - maxReplicas: 30 - metrics: - - resource: - name: cpu - target: - averageUtilization: 90 - type: Utilization - type: Resource - minReplicas: 2 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: minimal-wfs-mapserver diff --git a/internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml b/internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml deleted file mode 100644 index 7affcb3..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapserver - namespace: default - annotations: - uptime.pdok.nl/id: b333bf07fbf71b3ca2e55750b75d8a72309b9564 - uptime.pdok.nl/name: MINIMAL WFS - uptime.pdok.nl/tags: public-stats,wfs - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1 - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - routes: - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/wfs/v1_0`) - middlewares: - - name: minimal-wfs-mapserver-headers - services: - - kind: Service - name: minimal-wfs-mapserver - port: 80 diff --git a/internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml b/internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml deleted file mode 100644 index adab435..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml deleted file mode 100644 index 58f36f4..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 diff --git a/internal/controller/test_data/wfs/minimal/expected/service.yaml b/internal/controller/test_data/wfs/minimal/expected/service.yaml deleted file mode 100644 index 342df3f..0000000 --- a/internal/controller/test_data/wfs/minimal/expected/service.yaml +++ /dev/null @@ -1,40 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: minimal-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - internalTrafficPolicy: Cluster - sessionAffinity: None - type: ClusterIP - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 diff --git a/internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml b/internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml deleted file mode 100644 index 75a212e..0000000 --- a/internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wfs: - serviceProvider: - providerName: PDOK diff --git a/internal/controller/test_data/wfs/minimal/input/wfs.yaml b/internal/controller/test_data/wfs/minimal/input/wfs.yaml deleted file mode 100644 index a458f76..0000000 --- a/internal/controller/test_data/wfs/minimal/input/wfs.yaml +++ /dev/null @@ -1,71 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WFS -metadata: - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wfs - service-version: v1_0 - name: minimal - namespace: default -spec: - options: {} - podSpecPatch: - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - resources: - limits: - ephemeral-storage: 100M - service: - abstract: service-abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - bbox: - defaultCRS: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - defaultCrs: EPSG:28992 - featureTypes: - - abstract: featuretype-abstract - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: featuretype-column - geometryType: Point - tableName: featuretype - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - featuretype-keyword - name: featuretype-name - title: featuretype-title - keywords: - - service-keyword - otherCrs: - - EPSG:25831 - - EPSG:25832 - - EPSG:3034 - - EPSG:3035 - - EPSG:3857 - - EPSG:4258 - - EPSG:4326 - ownerInfoRef: owner - prefix: dataset - title: service-title - url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0 diff --git a/internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index 2c2561a..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -data: - input.yaml: "..." -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-capabilities-generator-m46924mtk7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml deleted file mode 100644 index 1f653ce..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v1 -data: - input.json: "..." -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapfile-generator-cdchdd74m7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml b/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml deleted file mode 100644 index 24c9ced..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -apiVersion: v1 -data: - default_mapserver.conf: "..." - include.conf: "..." - ogc.lua: "..." - scraping-error.xml: "..." -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapserver-f5ch9b2bhh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml b/internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml deleted file mode 100644 index bbf317b..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml +++ /dev/null @@ -1,245 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: '9117' - prometheus.io/scrape: 'true' - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - spec: - containers: - - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - - name: SERVICE_TYPE - value: WFS - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - lifecycle: - preStop: - exec: - command: - - sleep - - '15' - livenessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - name: mapserver - ports: - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - resources: - limits: - ephemeral-storage: 200M - memory: 800M - requests: - cpu: '0.15' - startupProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - args: - - --scrape_uri=http://localhost/server-status?auto - image: test.test/image:test5 - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: '0.02' - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - requests: - cpu: '0.15' - limits: - cpu: '0.2' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:test4 - imagePullPolicy: IfNotPresent - name: capabilities-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --not-include - - wfs - - /input/input.json - - /srv/data/config/mapfile - command: - - generate-mapfile - image: test.test/image:test2 - imagePullPolicy: IfNotPresent - name: mapfile-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: mapfile-generator-config - readOnly: true - restartPolicy: Always - terminationGracePeriodSeconds: 60 - dnsPolicy: ClusterFirst - volumes: - - emptyDir: {} - name: base - - emptyDir: {} - name: data - - configMap: - name: noprefetch-wfs-mapserver-f5ch9b2bhh - defaultMode: 420 - name: mapserver - - configMap: - name: noprefetch-wfs-capabilities-generator-m46924mtk7 - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: noprefetch-wfs-mapfile-generator-cdchdd74m7 - defaultMode: 420 - name: mapfile-generator-config diff --git a/internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index 1543321..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,52 +0,0 @@ ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 600 - type: Percent - value: 10 - - periodSeconds: 600 - type: Pods - value: 1 - selectPolicy: Max - stabilizationWindowSeconds: 3600 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 300 - maxReplicas: 30 - metrics: - - resource: - name: cpu - target: - averageUtilization: 90 - type: Utilization - type: Resource - minReplicas: 2 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: noprefetch-wfs-mapserver diff --git a/internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml b/internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml deleted file mode 100644 index 7b43d28..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml +++ /dev/null @@ -1,35 +0,0 @@ ---- -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapserver - namespace: default - annotations: - uptime.pdok.nl/id: 7bd64fef831d74baee2ef9158b98f786511e1bc6 - uptime.pdok.nl/name: NOPREFETCH WFS - uptime.pdok.nl/tags: public-stats,wfs - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1 - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - routes: - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/wfs/v1_0`) - middlewares: - - name: noprefetch-wfs-mapserver-headers - services: - - kind: Service - name: noprefetch-wfs-mapserver - port: 80 diff --git a/internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml b/internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml deleted file mode 100644 index 058beed..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml deleted file mode 100644 index d437eda..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wfs - service-version: v1_0 diff --git a/internal/controller/test_data/wfs/noprefetch/expected/service.yaml b/internal/controller/test_data/wfs/noprefetch/expected/service.yaml deleted file mode 100644 index 7e2675a..0000000 --- a/internal/controller/test_data/wfs/noprefetch/expected/service.yaml +++ /dev/null @@ -1,40 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 - name: noprefetch-wfs-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WFS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - internalTrafficPolicy: Cluster - sessionAffinity: None - type: ClusterIP - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wfs - service-version: v1_0 diff --git a/internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml b/internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml deleted file mode 100644 index 75a212e..0000000 --- a/internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wfs: - serviceProvider: - providerName: PDOK diff --git a/internal/controller/test_data/wfs/noprefetch/input/wfs.yaml b/internal/controller/test_data/wfs/noprefetch/input/wfs.yaml deleted file mode 100644 index d65548f..0000000 --- a/internal/controller/test_data/wfs/noprefetch/input/wfs.yaml +++ /dev/null @@ -1,72 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WFS -metadata: - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wfs - service-version: v1_0 - name: noprefetch - namespace: default -spec: - options: - prefetchData: false - podSpecPatch: - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - resources: - limits: - ephemeral-storage: 100M - service: - abstract: service-abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - bbox: - defaultCRS: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - defaultCrs: EPSG:28992 - featureTypes: - - abstract: featuretype-abstract - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: featuretype-column - geometryType: Point - tableName: featuretype - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - featuretype-keyword - name: featuretype-name - title: featuretype-title - keywords: - - service-keyword - otherCrs: - - EPSG:25831 - - EPSG:25832 - - EPSG:3034 - - EPSG:3035 - - EPSG:3857 - - EPSG:4258 - - EPSG:4326 - ownerInfoRef: owner - prefix: dataset - title: service-title - url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0 diff --git a/internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index 45e7cf9..0000000 --- a/internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,244 +0,0 @@ -apiVersion: v1 -data: - input.yaml: |- - global: - additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd - http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd - namespace: http://dataset.geonovum.nl - onlineresourceurl: http://localhost - path: /datasetOwner/dataset/2016 - prefix: dataset - version: v1_0 - services: - wms130: - definition: - capability: - wmscapabilities: - extendedcapabilities: - metadataurl: - mediatype: application/vnd.ogc.csw.GetRecordByIdResponse_xml - url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=metameta-meta-meta-meta-metametameta - responselanguage: - language: dut - supportedlanguages: - defaultlanguage: - language: dut - layer: - - abstract: Top "Layer" Abstract - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - top-layer-keyword-1 - - top-layer-keyword-2 - layer: - - abstract: group layer abstract "1" - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - group-layer-keyword-1 - - group-layer-keyword-2 - layer: - - abstract: gpkg-layer-abstract "2" - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - gpkg-layer-keyword-1 - - gpkg-layer-keyword-2 - maxscaledenominator: 20 - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - minscaledenominator: 30 - name: gpkg-layer-name - queryable: 1 - style: - - abstract: gpkg-layer-style-1-abstract - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/gpkg-layer-name/gpkg-layer-style-1-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: gpkg-layer-style-1-name - title: gpkg-layer-style-1-title - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/gpkg-layer-name/gpkg-layer-style-2-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: gpkg-layer-style-2-name - title: gpkg-layer-style-2-title - title: gpkg-layer-title "2" - - abstract: postgis-layer-abstract - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - postgis-layer-keyword-1 - - postgis-layer-keyword-2 - maxscaledenominator: 20 - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - minscaledenominator: 30 - name: postgis-layer-name - queryable: 1 - style: - - abstract: postgis-layer-style-1-abstract - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/postgis-layer-name/postgis-layer-style-1-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: postgis-layer-style-1-name - title: postgis-layer-style-1-title - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/postgis-layer-name/postgis-layer-style-2-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: postgis-layer-style-2-name - title: postgis-layer-style-2-title - title: postgis-layer-title - maxscaledenominator: 50 - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group-layer-name - queryable: 1 - style: - - abstract: group-layer-style-1-abstract - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/group-layer-name/group-layer-style-1-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group-layer-style-1-name - title: group-layer-style-1-title - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/group-layer-name/group-layer-style-2-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group-layer-style-2-name - title: group-layer-style-2-title - title: group layer title "1" - maxscaledenominator: 50 - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: top-layer-name - queryable: 1 - style: - - abstract: top-layer-style-1-abstract - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/top-layer-name/top-layer-style-1-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: top-layer-style-1-name - title: top-layer-style-1-title - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/top-layer-name/top-layer-style-2-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: top-layer-style-2-name - title: top-layer-style-2-title - title: Top "Layer" Title - service: - abstract: some "service" abstract - accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - keywordlist: - keyword: - - service-keyword-1 - - service-keyword-2 - - infoMapAccessService - optionalconstraints: - maxheight: 4000 - maxwidth: 4000 - title: some service title - filename: /var/www/config/capabilities_wms_130.xml - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-capabilities-generator-b9kmb96877 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml deleted file mode 100644 index 6880d7b..0000000 --- a/internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml +++ /dev/null @@ -1,72 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "Projection": "EPSG:28992", - "AutomaticCasing": true, - "Version": 2, - "Layers": [ - { - "Name": "gpkg-layer-name", - "GroupName": "group-layer-name", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "column-1", - "Alias": "ALIAS_column-1" - }, - { - "Name": "column-2" - } - ] - }, - { - "Name": "postgis-layer-name", - "GroupName": "group-layer-name", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "column-1" - }, - { - "Name": "column-2" - } - ] - }, - { - "Name": "tif-layer-name", - "Properties": [ - { - "Name": "value_list" - }, - { - "Name": "class" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-featureinfo-generator-257f6m6228 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml deleted file mode 100644 index 031880d..0000000 --- a/internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml +++ /dev/null @@ -1,190 +0,0 @@ ---- -apiVersion: v1 -data: - gpkg_download.sh: |- - #!/usr/bin/env bash - - set -euo pipefail - - function download_gpkg() { - local gpkg=$1 - local file=$2 - local url=$3 - - if [ -f "$file" ] && [ ! -f "$file".st ]; then - echo msg=\"File already downloaded\" file=\""$file"\" - else - echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" - - # use curl to check if resource exists - # axel blocks on non-existing resources - curl -IfsS "$url" > /dev/null - - echo start "$gpkg" - ret=0 - # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. - axel -n 1 -T 120 -o "$file" "$url" \ - | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ - | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? - - if [ $ret -ne 0 ] - then - echo -e '\n' - # Download failed ($? != 0). - if [ $ret -eq 1 ] - then - # Axel was not able to resume ($? == 1). Remove file and state file. - if [ -f "$file" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file"\" - rm "$file" - fi - if [ -f "$file.st" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file".st\" - rm "$file".st - fi - else - # Download failed with other error ($? > 1). Remove file if state file does not exist. - if [ ! -f "$file.st" ]; then - echo msg=\"Download failed without state file, removing file\" file=\""$file"\" - rm "$file" - fi - fi - - # Retry the download - echo msg=\"Retry file\" file=\""$file"\" - download_gpkg $gpkg $file $url - fi - fi - } - - function download() { - if [ -z "$BLOBS_ENDPOINT" ]; - then - echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; - exit 1; - fi - - local gpkg=$1 - local file=/srv/data/gpkg/$2 - local url=${BLOBS_ENDPOINT}/${gpkg} - - download_gpkg $gpkg $file $url - - # Check Content-length - download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') - file_size=$(wc -c "$file" | awk '{print $1}') - if [ "$download_size" != "$file_size" ] - then - echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - rm_file_and_exit - else - echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - chown 999:999 "$file" - fi - - # Check ogrinfo - echo "Check gpkg with ogrinfo" - if ! ogrinfo -so "$file" - then - echo "ERROR: ogrinfo check on $file failed" - rm_file_and_exit - fi - - # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) - echo "Check if md5 hash value exists in blob storage" - rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" - - # If file contains valid hash, then check it, else skip - hash=$(awk '{ print $1 }' "${file}.md5sum-remote") - if [[ $hash =~ ^[a-f0-9]{32}$ ]] - then - echo "Valid hash value found" - echo "Compare MD5 hash of remote and downloaded gpkg" - if ! (echo "$hash $file" | md5sum --check); then - rm_file_and_exit - fi - else - echo "No hash found for $file in blob storage, skipping checksum." - fi - - echo "done" - } - - function download_all() { - echo msg=\"Starting GeoPackage downloader\" - - local start_time=$(date '+%s') - - # create target location if not exists - mkdir -p /srv/data/gpkg - chown 999:999 /srv/data/gpkg - - download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; - - echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) - } - - function rm_file_and_exit() { - echo "Removing $file, to ensure a fresh new download is started when script is executed again" - rm -rf "$file" - - if [ -f "$file.st" ]; then - rm "$file".st - fi - - echo "Exiting..." - exit 1 - } - - download_all | awk -W interactive ' - BEGIN { - state="idle"; - } - - { - if ($0 != "") { - if ($1 == "start") { - gpkg=$2; - state="downloading"; - } else if ($1 == "done") { - state="idle"; - } else if (state == "downloading") { - if ($1 == "progress") { - # reduce output to prevent loki from choking on large log volume - if (last_percentage != $2) { - if ($3 == "") { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; - } else { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; - } - } - last_percentage=$2; - } else { - print "msg=\"" $0 "\" gpkg=" gpkg; - } - } else { - print $0; - } - } - } - ' -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: "2016" - name: complete-wms-init-scripts-f8k8ffgmgh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml deleted file mode 100644 index 86cb48a..0000000 --- a/internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml +++ /dev/null @@ -1,86 +0,0 @@ -apiVersion: v1 -data: - default_mapserver.conf: "..." - input: |- - "top-layer-name" "top-layer-style-1-name" - "top-layer-name" "top-layer-style-2-name" - "group-layer-name" "group-layer-style-1-name" - "group-layer-name" "group-layer-style-2-name" - "group-layer-name" "top-layer-style-1-name" - "gpkg-layer-name" "gpkg-layer-style-1-name" - "gpkg-layer-name" "gpkg-layer-style-2-name" - "gpkg-layer-name" "top-layer-style-1-name" - "gpkg-layer-name" "group-layer-style-2-name" - "postgis-layer-name" "postgis-layer-style-1-name" - "postgis-layer-name" "postgis-layer-style-2-name" - legend-fixer.sh: |- - #!/usr/bin/env bash - set -eo pipefail - echo "creating legends for root and group layers by concatenating data layers" - input_filepath="/input/input" - remove_filepath="/input/remove" - config_filepath="/input/ogc-webservice-proxy-config.yaml" - legend_dir="/var/www/legend" - < "${input_filepath}" xargs -n 2 echo | while read -r layer style; do - export layer - # shellcheck disable=SC2016 # dollar is for yq - if ! < "${config_filepath}" yq -e 'env(layer) as $layer | .grouplayers | keys | contains([$layer])' &>/dev/null; then - continue - fi - export grouplayer="${layer}" - grouplayer_style_filepath="${legend_dir}/${grouplayer}/${style}.png" - # shellcheck disable=SC2016 # dollar is for yq - datalayers=$(< "${config_filepath}" yq 'env(grouplayer) as $foo | .grouplayers[$foo][]') - datalayer_style_filepaths=() - for datalayer in $datalayers; do - datalayer_style_filepath="${legend_dir}/${datalayer}/${style}.png" - if [[ -f "${datalayer_style_filepath}" ]]; then - datalayer_style_filepaths+=("${datalayer_style_filepath}") - fi - done - if [[ -n "${datalayer_style_filepaths[*]}" ]]; then - echo "concatenating ${grouplayer_style_filepath}" - gm convert -append "${datalayer_style_filepaths[@]}" "${grouplayer_style_filepath}" - else - echo "no data for ${grouplayer_style_filepath}" - fi - done - < "${remove_filepath}" xargs -n 2 echo | while read -r layer style; do - remove_legend_file="${legend_dir}/${layer}/${style}.png" - echo removing $remove_legend_file - rm $remove_legend_file - done - echo "done" - ogc-webservice-proxy-config.yaml: |- - grouplayers: - group-layer-name: - - gpkg-layer-name - - postgis-layer-name - top-layer-name: - - gpkg-layer-name - - postgis-layer-name - - tif-layer-name - - remove: |- - "group-layer-name" "top-layer-style-1-name" - "gpkg-layer-name" "top-layer-style-1-name" -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-legend-generator-bmg7f9t24k - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml deleted file mode 100644 index bd1092f..0000000 --- a/internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml +++ /dev/null @@ -1,179 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "service_title": "some service title", - "service_abstract": "some \"service\" abstract", - "service_keywords": "service-keyword-1,service-keyword-2,infoMapAccessService", - "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "service-extent", - "maxSize": "4000", - "service_namespace_prefix": "dataset", - "service_namespace_uri": "http://dataset.geonovum.nl", - "service_onlineresource": "http://localhost", - "service_path": "/datasetOwner/dataset/2016/wms/v1_0", - "service_metadata_id": "metameta-meta-meta-meta-metametameta", - "dataset_owner": "authority-name", - "authority_url": "http://authority-url", - "automatic_casing": true, - "data_epsg": "EPSG:28992", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "templates": "/srv/data/config/templates", - "fonts": "/srv/data/config/fonts", - "top_level_name": "top-layer-name", - "resolution": "20", - "defresolution": "10", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "symbols": [ - "/styling/gpkg-layer-symbol.symbol", - "/styling/tif-layer-symbol.symbol" - ], - "group_layers": [ - { - "name": "group-layer-name", - "title": "group layer title \"1\"", - "abstract": "group layer abstract \"1\"", - "style_name": "group-layer-style-1-name", - "style_title": "group-layer-style-1-title" - } - ], - "layers": [ - { - "name": "gpkg-layer-name", - "group_name": "group-layer-name", - "gpkg_path": "/srv/data/gpkg/file.gpkg", - "tablename": "gpkg-layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "column-1", - "alias": "ALIAS_column-1" - }, - { - "name": "column-2" - } - ], - "title": "gpkg-layer-title \"2\"", - "abstract": "gpkg-layer-abstract \"2\"", - "keywords": "gpkg-layer-keyword-1,gpkg-layer-keyword-2", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "layer_extent": "gpkg-layer-extent", - "minscale": "30", - "maxscale": "20", - "styles": [ - { - "title": "gpkg-layer-style-1-title", - "path": "/styling/gpkg-layer-style-1.style" - }, - { - "title": "gpkg-layer-style-2-title", - "path": "/styling/gpkg-layer-style-2.style" - }, - { - "title": "gpkg-layer-style-3-title", - "path": "/styling/gpkg-layer-style-3.style" - }, - { - "title": "gpkg-layer-style-4-title", - "path": "/styling/gpkg-layer-style-4.style" - } - ] - }, - { - "name": "postgis-layer-name", - "group_name": "group-layer-name", - "tablename": "postgis-layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "column-1" - }, - { - "name": "column-2" - } - ], - "postgis": true, - "title": "postgis-layer-title", - "abstract": "postgis-layer-abstract", - "keywords": "postgis-layer-keyword-1,postgis-layer-keyword-2", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "layer_extent": "postgis-layer-extent", - "minscale": "30", - "maxscale": "20", - "styles": [ - { - "title": "postgis-layer-style-1-title", - "path": "/styling/postgis-layer-style-1.style" - }, - { - "title": "postgis-layer-style-2-title", - "path": "/styling/postgis-layer-style-2.style" - } - ] - }, - { - "name": "tif-layer-name", - "resample": "AVERAGE", - "tif_path": "/srv/data/tif/file.tif", - "geometry_type": "Raster", - "offsite": "#FF00FF", - "get_feature_info_includes_class": true, - "title": "", - "abstract": "", - "keywords": "", - "dataset_metadata_id": "", - "dataset_source_id": "", - "layer_extent": "tif-layer-extent", - "minscale": "30", - "maxscale": "20", - "label_no_clip": true, - "styles": [ - { - "path": "/styling/tif-layer-style-1.style" - }, - { - "path": "/styling/tif-layer-style-2.style" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-mapfile-generator-gh2fg6ccm9 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml deleted file mode 100644 index d279042..0000000 --- a/internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,147 +0,0 @@ ---- -apiVersion: v1 -data: - default_mapserver.conf: |- - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - MAPS - MAP "/srv/data/config/mapfile/service.map" - END - END - include.conf: >- - server.modules += ( "mod_status" ) - - - $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { - status.status-url = "/server-status" - } - - - url.rewrite-once = ( - "/datasetOwner/dataset/2016/wms/v1_0/legend(.*)" => "/legend$1", - "/datasetOwner/dataset/2016/wms/v1_0(.*)" => "/mapserver$1" - ) - - - magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) - - - setenv.add-environment += ( - "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, - ) - ogc.lua: >- - if lighty.r.req_attr["request.method"] == "GET" then - - -- obtain service type from environment - serviceType = os.getenv('SERVICE_TYPE'):lower() - - path = lighty.r.req_attr["uri.path"] - query = lighty.r.req_attr["uri.query"] - - -- handle legend requests - if serviceType == "wms" then - _, _, file = path:find(".*/legend/(.*)") - if file then - if file:find(".*%.png") then - local legendPath = "/var/www/legend/" .. file - local stat = lighty.stat(legendPath) - if (not stat or not stat.is_file) then - -- don't serve non existing legend file - return 404 - end - lighty.content = { { filename = legendPath } } - lighty.header['Content-Type'] = "image/png" - return 200 - end - - return 404 - end - end - - params = {} - if query then - for k, v in query:gmatch("([^?&=]+)=([^&]+)") do - k = k:lower() - - params[k] = v - end - end - - -- assign service and version default values - version = params['version'] - service = params['service'] - - if not service then - service = serviceType - else - service = service:lower() - end - - if (service == 'wms' and (not version or version ~= '1.1.1')) then - version = '1.3.0' - end - - if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then - version = '2.0.0' - end - - -- serve static content - request = params['request'] - if request then - request = request:lower() - - staticStatus = 200 - staticContentType = 'text/xml; charset=UTF-8' - if request == 'getcapabilities' then - if (service == 'wms' and version == '1.3.0') then - staticFile = '/var/www/config/capabilities_wms_130.xml' - elseif (service == 'wfs' and version == '2.0.0') then - staticFile = '/var/www/config/capabilities_wfs_200.xml' - end - elseif service == 'wfs' and request == 'getfeature' then - startindex = params['startindex'] - if startindex and tonumber(startindex) > 50000 then - staticFile = '/srv/mapserver/config/scraping-error.xml' - staticStatus = 400 - end - end - - if staticFile then - lighty.content = { { filename = staticFile } } - lighty.header['Content-Type'] = staticContentType - return staticStatus - end - end - end - scraping-error.xml: >- - - - - - - It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. - - - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: "2016" - name: complete-wms-mapserver-88ckd472mk - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml deleted file mode 100644 index 06d367d..0000000 --- a/internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: v1 -data: - service-config.yaml: |- - grouplayers: - group-layer-name: - - gpkg-layer-name - - postgis-layer-name - top-layer-name: - - gpkg-layer-name - - postgis-layer-name - - tif-layer-name - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-ogc-webservice-proxy-8d98h664bh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/complete/expected/deployment.yaml b/internal/controller/test_data/wms/complete/expected/deployment.yaml deleted file mode 100644 index f2cbbdc..0000000 --- a/internal/controller/test_data/wms/complete/expected/deployment.yaml +++ /dev/null @@ -1,404 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: '9117' - prometheus.io/scrape: 'true' - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - spec: - restartPolicy: Always - dnsPolicy: ClusterFirst - containers: - - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - - name: SERVICE_TYPE - value: WMS - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - lifecycle: - preStop: - exec: - command: - - sleep - - '15' - livenessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabilities'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - name: mapserver - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=11,22,33,44&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=gpkg-layer-name&STYLES=&FORMAT=image/png'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: image/png''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - resources: - limits: - cpu: '4' - memory: 100M - requests: - cpu: '2' - memory: 50M - startupProbe: - exec: - command: - - /bin/sh - - -c - - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=11,22,33,44&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=top-layer-name,group-layer-name,gpkg-layer-name,postgis-layer-name,tif-layer-name&STYLES=&FORMAT=image/png'' - 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: image/png''' - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - args: - - --scrape_uri=http://localhost/server-status?auto - image: test.test/image:test7 - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: '0.02' - - command: - - /ogc-webservice-proxy - - -h=http://127.0.0.1/ - - -t=wms - - -s=/input/service-config.yaml - - -v - - -r - - -d=15 - image: test.test/image:test6 - imagePullPolicy: IfNotPresent - name: ogc-webservice-proxy - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9111 - resources: - limits: - memory: 200M - requests: - cpu: '0.05' - volumeMounts: - - mountPath: /input - name: ogc-webservice-proxy-config - readOnly: true - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - bash /srv/scripts/gpkg_download.sh; - rclone copyto blobs:/${BLOBS_TIF_BUCKET}/key/file.tif /srv/data/tif/file.tif || exit 1; - rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/tif-symbol.png /srv/data/images/tif-symbol.png || exit 1; - rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/gpkg-symbol.png /srv/data/images/gpkg-symbol.png || exit 1; - rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/symbol.svg /srv/data/images/symbol.svg || exit 1; - rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/font-1.ttf /srv/data/config/fonts/font-1.ttf || exit 1; - echo font-1 font-1.ttf >> /srv/data/config/fonts/fonts.list; - rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/font-2.ttf /srv/data/config/fonts/font-2.ttf || exit 1; - echo font-2 font-2.ttf >> /srv/data/config/fonts/fonts.list; - echo 'generated fonts.list:'; - cat /srv/data/config/fonts/fonts.list; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - requests: - cpu: '0.15' - limits: - cpu: '1' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - mountPath: /srv/scripts - name: init-scripts - readOnly: true - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:test4 - imagePullPolicy: IfNotPresent - name: capabilities-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --not-include - - wms - - /input/input.json - - /srv/data/config/mapfile - command: - - generate-mapfile - envFrom: - - configMapRef: - name: postgres-testtest - - secretRef: - name: postgres-testtest - image: test.test/image:test2 - imagePullPolicy: IfNotPresent - name: mapfile-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: mapfile-generator-config - readOnly: true - - mountPath: /styling - name: styling-files - readOnly: true - - args: - - --input-path - - /input/input.json - - --dest-folder - - /srv/data/config/templates - - --file-name - - feature-info - command: - - featureinfo-generator - image: test.test/image:test5 - imagePullPolicy: IfNotPresent - name: featureinfo-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: featureinfo-generator-config - readOnly: true - - command: - - bash - - -c - - | - set -eu; - exit_code=0; - cat /input/input | xargs -n 2 echo | while read layer style; do - echo Generating legend for layer: $layer, style: $style; - mkdir -p /var/www/legend/$layer; - mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; - magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); - if [[ $magic_bytes != 'PNG' ]]; then - echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; - exit_code=1; - fi; - done; - exit $exit_code; - env: - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - name: legend-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /input - name: legend-generator-config - readOnly: true - - command: - - /bin/bash - - /input/legend-fixer.sh - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: legend-fixer - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: legend-generator-config - readOnly: true - terminationGracePeriodSeconds: 60 - volumes: - - ephemeral: - volumeClaimTemplate: - spec: - accessModes: - - ReadWriteOnce - storageClassName: test-storage - resources: - requests: - storage: 11G - name: base - - emptyDir: {} - name: data - - configMap: - name: complete-wms-mapserver-88ckd472mk - defaultMode: 420 - name: mapserver - - configMap: - name: complete-wms-ogc-webservice-proxy-8d98h664bh - defaultMode: 420 - name: ogc-webservice-proxy-config - - configMap: - defaultMode: 511 - name: complete-wms-init-scripts-f8k8ffgmgh - name: init-scripts - - configMap: - name: complete-wms-capabilities-generator-b9kmb96877 - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: complete-wms-mapfile-generator-gh2fg6ccm9 - defaultMode: 420 - name: mapfile-generator-config - - name: styling-files - projected: - sources: - - configMap: - name: gpkg-styling - - configMap: - name: tif-styling - - configMap: - name: postgis-styling - - configMap: - name: complete-wms-featureinfo-generator-257f6m6228 - defaultMode: 420 - name: featureinfo-generator-config - - configMap: - name: complete-wms-legend-generator-bmg7f9t24k - defaultMode: 420 - name: legend-generator-config diff --git a/internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index bfd17e6..0000000 --- a/internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,53 +0,0 @@ ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: "2016" - name: complete-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 600 - type: Percent - value: 10 - - periodSeconds: 600 - type: Pods - value: 1 - selectPolicy: Max - stabilizationWindowSeconds: 3600 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 0 - maxReplicas: 50 - metrics: - - resource: - name: cpu - target: - averageUtilization: 20 - type: Utilization - type: Resource - minReplicas: 1 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: complete-wms-mapserver diff --git a/internal/controller/test_data/wms/complete/expected/ingressroute.yaml b/internal/controller/test_data/wms/complete/expected/ingressroute.yaml deleted file mode 100644 index ab4ed9b..0000000 --- a/internal/controller/test_data/wms/complete/expected/ingressroute.yaml +++ /dev/null @@ -1,62 +0,0 @@ ---- -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: "2016" - name: complete-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true - annotations: - uptime.pdok.nl/id: 5b67e76cef85f33507d2ff00ddd73fe85d4eb449 - uptime.pdok.nl/name: COMPLETE INSPIRE WMS - uptime.pdok.nl/tags: public-stats,wms,inspire - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/2016/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=11,22,33,44&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=gpkg-layer-name&STYLES=&FORMAT=image/png -spec: - routes: - - kind: Rule - match: Host(`localhost`) && - PathPrefix(`/datasetOwner/dataset/2016/wms/v1_0/legend`) - middlewares: - - name: complete-wms-mapserver-headers - services: - - kind: Service - name: complete-wms-mapserver - port: 80 - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/2016/wms/v1_0`) - middlewares: - - name: complete-wms-mapserver-headers - services: - - kind: Service - name: complete-wms-mapserver - port: 9111 - - kind: Rule - match: Host(`localhost`) && - PathPrefix(`/other/path/legend`) - middlewares: - - name: complete-wms-mapserver-headers - services: - - kind: Service - name: complete-wms-mapserver - port: 80 - - kind: Rule - match: Host(`localhost`) && Path(`/other/path`) - middlewares: - - name: complete-wms-mapserver-headers - services: - - kind: Service - name: complete-wms-mapserver - port: 9111 diff --git a/internal/controller/test_data/wms/complete/expected/middleware-headers.yaml b/internal/controller/test_data/wms/complete/expected/middleware-headers.yaml deleted file mode 100644 index 1587754..0000000 --- a/internal/controller/test_data/wms/complete/expected/middleware-headers.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml deleted file mode 100644 index babbc52..0000000 --- a/internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' - name: complete-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: '2016' diff --git a/internal/controller/test_data/wms/complete/expected/service.yaml b/internal/controller/test_data/wms/complete/expected/service.yaml deleted file mode 100644 index 9aa1fcf..0000000 --- a/internal/controller/test_data/wms/complete/expected/service.yaml +++ /dev/null @@ -1,44 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: "2016" - name: complete-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: complete - uid: "" - blockOwnerDeletion: true - controller: true -spec: - type: ClusterIP - sessionAffinity: None - internalTrafficPolicy: Cluster - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: ogc-webservice-proxy - port: 9111 - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "true" - service-type: wms - service-version: v1_0 - theme: "2016" diff --git a/internal/controller/test_data/wms/complete/input/ownerinfo.yaml b/internal/controller/test_data/wms/complete/input/ownerinfo.yaml deleted file mode 100644 index 8025b4a..0000000 --- a/internal/controller/test_data/wms/complete/input/ownerinfo.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wms: - contactInformation: - contactPersonPrimary: - contactPerson: KlantContactCenter PDOK - contactOrganization: PDOK - contactPosition: pointOfContact - contactAddress: - addressType: - address: - city: Apeldoorn - stateOrProvince: - postCode: - country: Netherlands - contactVoiceTelephone: - contactFacsimileTelephone: - contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/complete/input/wms.yaml b/internal/controller/test_data/wms/complete/input/wms.yaml deleted file mode 100644 index 142eaa2..0000000 --- a/internal/controller/test_data/wms/complete/input/wms.yaml +++ /dev/null @@ -1,308 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - creationTimestamp: "2022-09-01T12:00:00Z" - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wms - service-version: v1_0 - theme: 2016 - name: complete - namespace: default -spec: - ingressRouteUrls: - - url: http://localhost:32788/datasetOwner/dataset/2016/wms/v1_0 - - url: http://localhost:32788/other/path - healthCheck: - boundingbox: - maxx: "33" - maxy: "44" - minx: "11" - miny: "22" - horizontalPodAutoscalerPatch: - maxReplicas: 50 - metrics: - - resource: - name: cpu - target: - averageUtilization: 20 - type: Utilization - type: Resource - minReplicas: 1 - lifecycle: - ttlInDays: 730000 - options: - rewriteGroupToDataLayers: true - podSpecPatch: - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - - name: mapfile-generator - envFrom: - - configMapRef: - name: postgres-testtest - - secretRef: - name: postgres-testtest - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - resources: - limits: - cpu: "4" - ephemeral-storage: 11G - memory: 100M - requests: - cpu: "2" - ephemeral-storage: 11G - memory: 50M - service: - prefix: dataset - abstract: some "service" abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - defResolution: 10 - inspire: - language: dut - serviceMetadataUrl: - csw: - metadataIdentifier: metameta-meta-meta-meta-metametameta - keywords: - - service-keyword-1 - - service-keyword-2 - layer: - abstract: Top "Layer" Abstract - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - boundingBoxes: - - bbox: - maxx: "3" - maxy: "4" - minx: "1" - miny: "2" - crs: EPSG:28992 - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - top-layer-keyword-1 - - top-layer-keyword-2 - layers: - - abstract: group layer abstract "1" - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - boundingBoxes: - - bbox: - maxx: "3" - maxy: "4" - minx: "1" - miny: "2" - crs: EPSG:28992 - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - group-layer-keyword-1 - - group-layer-keyword-2 - layers: - - abstract: gpkg-layer-abstract "2" - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - boundingBoxes: - - bbox: - maxx: "7" - maxy: "8" - minx: "5" - miny: "6" - crs: EPSG:28992 - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - alias: ALIAS_column-1 - name: column-1 - - name: column-2 - geometryType: Point - tableName: gpkg-layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - gpkg-layer-keyword-1 - - gpkg-layer-keyword-2 - maxscaledenominator: "20" - minscaledenominator: "30" - name: gpkg-layer-name - styles: - - abstract: gpkg-layer-style-1-abstract - name: gpkg-layer-style-1-name - title: gpkg-layer-style-1-title - visualization: gpkg-layer-style-1.style - - name: gpkg-layer-style-2-name - title: gpkg-layer-style-2-title - visualization: gpkg-layer-style-2.style - - abstract: gpkg-layer-style-3-abstract - name: top-layer-style-1-name - title: gpkg-layer-style-3-title - visualization: gpkg-layer-style-3.style - - abstract: gpkg-layer-style-4-abstract - name: group-layer-style-2-name - title: gpkg-layer-style-4-title - visualization: gpkg-layer-style-4.style - - name: top-layer-style-2-name - visualization: top-layer-style-2.style - - name: group-layer-style-1-name - visualization: group-layer-style-1.style - - name: group-layer-style-3-name - visualization: group-layer-style-3.style - title: gpkg-layer-title "2" - visible: true - - abstract: postgis-layer-abstract - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - boundingBoxes: - - bbox: - maxx: "5" - maxy: "7" - minx: "1" - miny: "3" - crs: EPSG:28992 - data: - postgis: - columns: - - name: column-1 - - name: column-2 - geometryType: Point - tableName: postgis-layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - postgis-layer-keyword-1 - - postgis-layer-keyword-2 - maxscaledenominator: "20" - minscaledenominator: "30" - name: postgis-layer-name - styles: - - abstract: postgis-layer-style-1-abstract - name: postgis-layer-style-1-name - title: postgis-layer-style-1-title - visualization: postgis-layer-style-1.style - - name: postgis-layer-style-2-name - title: postgis-layer-style-2-title - visualization: postgis-layer-style-2.style - - name: top-layer-style-1-name - visualization: top-layer-style-1.style - - name: top-layer-style-2-name - visualization: top-layer-style-2.style - - name: group-layer-style-1-name - visualization: group-layer-style-1.style - - name: group-layer-style-2-name - visualization: group-layer-style-1.style - - name: group-layer-style-3-name - visualization: group-layer-style-1.style - title: postgis-layer-title - visible: true - maxscaledenominator: "50" - name: group-layer-name - styles: - - abstract: group-layer-style-1-abstract - name: group-layer-style-1-name - title: group-layer-style-1-title - - name: group-layer-style-2-name - title: group-layer-style-2-title - - abstract: group-layer-style-3-abstract - name: group-layer-style-3-name - title: group-layer-style-3-title - title: group layer title "1" - visible: true - - boundingBoxes: - - bbox: - maxx: "6" - maxy: "8" - minx: "2" - miny: "4" - crs: EPSG:28992 - keywords: - - keyword - title: title - abstract: abstract - data: - tif: - blobKey: ${BLOBS_TIF_BUCKET}/key/file.tif - getFeatureInfoIncludesClass: true - offsite: '#FF00FF' - resample: AVERAGE - labelNoClip: true - maxscaledenominator: "20" - minscaledenominator: "30" - name: tif-layer-name - styles: - - name: tif-layer-style-1-name - visualization: tif-layer-style-1.style - - name: tif-layer-style-2-name - visualization: tif-layer-style-2.style - - name: top-layer-style-1-name - visualization: top-layer-style-1.style - - name: top-layer-style-2-name - visualization: top-layer-style-2.style - visible: false - maxscaledenominator: "50" - name: top-layer-name - styles: - - abstract: top-layer-style-1-abstract - name: top-layer-style-1-name - title: top-layer-style-1-title - - name: top-layer-style-2-name - title: top-layer-style-2-title - title: Top "Layer" Title - visible: true - ownerInfoRef: owner - resolution: 20 - stylingAssets: - blobKeys: - - ${BLOBS_RESOURCES_BUCKET}/key/tif-symbol.png - - ${BLOBS_RESOURCES_BUCKET}/key/gpkg-symbol.png - - ${BLOBS_RESOURCES_BUCKET}/key/symbol.svg - - ${BLOBS_RESOURCES_BUCKET}/key/font-1.ttf - - ${BLOBS_RESOURCES_BUCKET}/key/font-2.ttf - configMapRefs: - - keys: - - gpkg-layer-style-1.style - - gpkg-layer-style-2.style - - gpkg-layer-style-3.style - - gpkg-layer-style-4.style - - gpkg-layer-symbol.symbol - - top-layer-style-1.style - - top-layer-style-2.style - - group-layer-style-1.style - - group-layer-style-3.style - name: gpkg-styling - - keys: - - tif-layer-style-1.style - - tif-layer-style-2.style - - tif-layer-symbol.symbol - name: tif-styling - - keys: - - postgis-layer-style-1.style - - postgis-layer-style-2.style - name: postgis-styling - title: some service title - url: http://localhost:32788/datasetOwner/dataset/2016/wms/v1_0 diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index 109de65..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,150 +0,0 @@ ---- -apiVersion: v1 -data: - input.yaml: >- - global: - additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd - http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd - namespace: http://dataset.geonovum.nl - onlineresourceurl: http://localhost - path: /datasetOwner/dataset - prefix: dataset - version: v1_0 - services: - wms130: - definition: - capability: - wmscapabilities: - layer: - - abstract: service-abstract - keywordlist: - keyword: - - service-keyword - layer: - - abstract: layer-abstract - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - layer-keyword - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: layer-name - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: layer-style-name - title: layer-style-title - title: layer-title - - abstract: group - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - layer: - - abstract: group-child - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group-child - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group-child - title: group-child - title: group-child - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group - title: group - title: group - queryable: 1 - title: service-title - service: - abstract: service-abstract - accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - keywordlist: - keyword: - - service-keyword - optionalconstraints: - maxheight: 4000 - maxwidth: 4000 - title: service-title - filename: /var/www/config/capabilities_wms_130.xml -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-capabilities-generator-865bt77thd - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml deleted file mode 100644 index c52c3e1..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "Projection": "EPSG:28992", - "AutomaticCasing": false, - "Version": 2, - "Layers": [ - { - "Name": "layer-name", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - }, - { - "Name": "group-child", - "GroupName": "group", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-featureinfo-generator-668mmh48cc - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml deleted file mode 100644 index 7a7f58d..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml +++ /dev/null @@ -1,189 +0,0 @@ ---- -apiVersion: v1 -data: - gpkg_download.sh: |- - #!/usr/bin/env bash - - set -euo pipefail - - function download_gpkg() { - local gpkg=$1 - local file=$2 - local url=$3 - - if [ -f "$file" ] && [ ! -f "$file".st ]; then - echo msg=\"File already downloaded\" file=\""$file"\" - else - echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" - - # use curl to check if resource exists - # axel blocks on non-existing resources - curl -IfsS "$url" > /dev/null - - echo start "$gpkg" - ret=0 - # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. - axel -n 1 -T 120 -o "$file" "$url" \ - | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ - | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? - - if [ $ret -ne 0 ] - then - echo -e '\n' - # Download failed ($? != 0). - if [ $ret -eq 1 ] - then - # Axel was not able to resume ($? == 1). Remove file and state file. - if [ -f "$file" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file"\" - rm "$file" - fi - if [ -f "$file.st" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file".st\" - rm "$file".st - fi - else - # Download failed with other error ($? > 1). Remove file if state file does not exist. - if [ ! -f "$file.st" ]; then - echo msg=\"Download failed without state file, removing file\" file=\""$file"\" - rm "$file" - fi - fi - - # Retry the download - echo msg=\"Retry file\" file=\""$file"\" - download_gpkg $gpkg $file $url - fi - fi - } - - function download() { - if [ -z "$BLOBS_ENDPOINT" ]; - then - echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; - exit 1; - fi - - local gpkg=$1 - local file=/srv/data/gpkg/$2 - local url=${BLOBS_ENDPOINT}/${gpkg} - - download_gpkg $gpkg $file $url - - # Check Content-length - download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') - file_size=$(wc -c "$file" | awk '{print $1}') - if [ "$download_size" != "$file_size" ] - then - echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - rm_file_and_exit - else - echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - chown 999:999 "$file" - fi - - # Check ogrinfo - echo "Check gpkg with ogrinfo" - if ! ogrinfo -so "$file" - then - echo "ERROR: ogrinfo check on $file failed" - rm_file_and_exit - fi - - # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) - echo "Check if md5 hash value exists in blob storage" - rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" - - # If file contains valid hash, then check it, else skip - hash=$(awk '{ print $1 }' "${file}.md5sum-remote") - if [[ $hash =~ ^[a-f0-9]{32}$ ]] - then - echo "Valid hash value found" - echo "Compare MD5 hash of remote and downloaded gpkg" - if ! (echo "$hash $file" | md5sum --check); then - rm_file_and_exit - fi - else - echo "No hash found for $file in blob storage, skipping checksum." - fi - - echo "done" - } - - function download_all() { - echo msg=\"Starting GeoPackage downloader\" - - local start_time=$(date '+%s') - - # create target location if not exists - mkdir -p /srv/data/gpkg - chown 999:999 /srv/data/gpkg - - download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; - - echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) - } - - function rm_file_and_exit() { - echo "Removing $file, to ensure a fresh new download is started when script is executed again" - rm -rf "$file" - - if [ -f "$file.st" ]; then - rm "$file".st - fi - - echo "Exiting..." - exit 1 - } - - download_all | awk -W interactive ' - BEGIN { - state="idle"; - } - - { - if ($0 != "") { - if ($1 == "start") { - gpkg=$2; - state="downloading"; - } else if ($1 == "done") { - state="idle"; - } else if (state == "downloading") { - if ($1 == "progress") { - # reduce output to prevent loki from choking on large log volume - if (last_percentage != $2) { - if ($3 == "") { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; - } else { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; - } - } - last_percentage=$2; - } else { - print "msg=\"" $0 "\" gpkg=" gpkg; - } - } else { - print $0; - } - } - } - ' -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-init-scripts-f8k8ffgmgh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml deleted file mode 100644 index 2530012..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -data: - default_mapserver.conf: ... - input: |- - "layer-name" "layer-style-name" - "group" "group" - "group-child" "group-child" -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-legend-generator-82hh8mg962 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml deleted file mode 100644 index cf06f96..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,146 +0,0 @@ ---- -apiVersion: v1 -data: - default_mapserver.conf: |- - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - MAPS - MAP "/srv/data/config/mapfile/service.map" - END - END - include.conf: >- - server.modules += ( "mod_status" ) - - - $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { - status.status-url = "/server-status" - } - - - url.rewrite-once = ( - "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", - "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" - ) - - - magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) - - - setenv.add-environment += ( - "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, - ) - ogc.lua: >- - if lighty.r.req_attr["request.method"] == "GET" then - - -- obtain service type from environment - serviceType = os.getenv('SERVICE_TYPE'):lower() - - path = lighty.r.req_attr["uri.path"] - query = lighty.r.req_attr["uri.query"] - - -- handle legend requests - if serviceType == "wms" then - _, _, file = path:find(".*/legend/(.*)") - if file then - if file:find(".*%.png") then - local legendPath = "/var/www/legend/" .. file - local stat = lighty.stat(legendPath) - if (not stat or not stat.is_file) then - -- don't serve non existing legend file - return 404 - end - lighty.content = { { filename = legendPath } } - lighty.header['Content-Type'] = "image/png" - return 200 - end - - return 404 - end - end - - params = {} - if query then - for k, v in query:gmatch("([^?&=]+)=([^&]+)") do - k = k:lower() - - params[k] = v - end - end - - -- assign service and version default values - version = params['version'] - service = params['service'] - - if not service then - service = serviceType - else - service = service:lower() - end - - if (service == 'wms' and (not version or version ~= '1.1.1')) then - version = '1.3.0' - end - - if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then - version = '2.0.0' - end - - -- serve static content - request = params['request'] - if request then - request = request:lower() - - staticStatus = 200 - staticContentType = 'text/xml; charset=UTF-8' - if request == 'getcapabilities' then - if (service == 'wms' and version == '1.3.0') then - staticFile = '/var/www/config/capabilities_wms_130.xml' - elseif (service == 'wfs' and version == '2.0.0') then - staticFile = '/var/www/config/capabilities_wfs_200.xml' - end - elseif service == 'wfs' and request == 'getfeature' then - startindex = params['startindex'] - if startindex and tonumber(startindex) > 50000 then - staticFile = '/srv/mapserver/config/scraping-error.xml' - staticStatus = 400 - end - end - - if staticFile then - lighty.content = { { filename = staticFile } } - lighty.header['Content-Type'] = staticContentType - return staticStatus - end - end - end - scraping-error.xml: >- - - - - - - It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. - - - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-mapserver-df94mb2d76 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml deleted file mode 100644 index a4389cf..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: v1 -data: - service-config.yaml: |- - grouplayers: - group-layer-name: - - gpkg-layer-name - - postgis-layer-name - top-layer-name: - - gpkg-layer-name - - postgis-layer-name - - tif-layer-name - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-ogc-webservice-proxy-22tb5878f7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml deleted file mode 100644 index 70a103e..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml +++ /dev/null @@ -1,341 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: "9117" - prometheus.io/scrape: "true" - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - spec: - restartPolicy: Always - dnsPolicy: ClusterFirst - containers: - - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - - name: SERVICE_TYPE - value: WMS - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/mapfile.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - lifecycle: - preStop: - exec: - command: - - sleep - - "15" - livenessProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabil\ - ities' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: text/xml'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - name: mapserver - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png' - 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: image/png'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - resources: - limits: - ephemeral-storage: 200M - memory: 800M - requests: - cpu: "0.1" - startupProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name,group,group-child&STYLES=&FORMAT=image/png' - 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: image/png'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - name: mapfile - mountPath: /srv/data/config/mapfile - - args: - - --scrape_uri=http://localhost/server-status?auto - image: test.test/image:test7 - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: "0.02" - - name: ogc-webservice-proxy - image: test.test/image:test6 - imagePullPolicy: IfNotPresent - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - limits: - memory: 200M - requests: - cpu: "0.05" - command: - - /ogc-webservice-proxy - - -h=http://127.0.0.1/ - - -t=wms - - -s=/input/service-config.yaml - - -v - - -d=15 - ports: - - containerPort: 9111 - volumeMounts: - - name: ogc-webservice-proxy-config - mountPath: /input - readOnly: true - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - bash /srv/scripts/gpkg_download.sh; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - requests: - cpu: '0.15' - limits: - cpu: '0.2' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - mountPath: /srv/scripts - name: init-scripts - readOnly: true - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:test4 - imagePullPolicy: IfNotPresent - name: capabilities-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --input-path - - /input/input.json - - --dest-folder - - /srv/data/config/templates - - --file-name - - feature-info - command: - - featureinfo-generator - image: test.test/image:test5 - imagePullPolicy: IfNotPresent - name: featureinfo-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: featureinfo-generator-config - readOnly: true - - command: - - bash - - -c - - | - set -eu; - exit_code=0; - cat /input/input | xargs -n 2 echo | while read layer style; do - echo Generating legend for layer: $layer, style: $style; - mkdir -p /var/www/legend/$layer; - mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; - magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); - if [[ $magic_bytes != 'PNG' ]]; then - echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; - exit_code=1; - fi; - done; - exit $exit_code; - env: - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/mapfile.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - name: legend-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/default_mapserver.conf - name: mapserver - subPath: default_mapserver.conf - - name: mapfile - mountPath: /srv/data/config/mapfile - - mountPath: /input - name: legend-generator-config - readOnly: true - terminationGracePeriodSeconds: 60 - volumes: - - emptyDir: {} - name: base - - emptyDir: {} - name: data - - configMap: - name: custom-mapfile-wms-mapserver-df94mb2d76 - defaultMode: 420 - name: mapserver - - configMap: - name: configMap - defaultMode: 420 - name: mapfile - - configMap: - name: custom-mapfile-wms-ogc-webservice-proxy-22tb5878f7 - defaultMode: 420 - name: ogc-webservice-proxy-config - - configMap: - defaultMode: 511 - name: custom-mapfile-wms-init-scripts-f8k8ffgmgh - name: init-scripts - - configMap: - name: custom-mapfile-wms-capabilities-generator-865bt77thd - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: custom-mapfile-wms-featureinfo-generator-668mmh48cc - defaultMode: 420 - name: featureinfo-generator-config - - configMap: - name: custom-mapfile-wms-legend-generator-82hh8mg962 - defaultMode: 420 - name: legend-generator-config diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index 10ebb09..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,52 +0,0 @@ ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 600 - type: Percent - value: 10 - - periodSeconds: 600 - type: Pods - value: 1 - selectPolicy: Max - stabilizationWindowSeconds: 3600 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 0 - maxReplicas: 30 - metrics: - - resource: - name: cpu - target: - averageUtilization: 90 - type: Utilization - type: Resource - minReplicas: 2 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: custom-mapfile-wms-mapserver diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml deleted file mode 100644 index d173bd8..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true - annotations: - "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" - uptime.pdok.nl/id: 327614531e386400ce221d6b9fc6d93dc252f0d3 - uptime.pdok.nl/name: CUSTOM mapfile WMS - uptime.pdok.nl/tags: public-stats,wms - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png -spec: - routes: - - kind: Rule - match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) - middlewares: - - name: custom-mapfile-wms-mapserver-headers - services: - - kind: Service - name: custom-mapfile-wms-mapserver - port: 80 - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) - middlewares: - - name: custom-mapfile-wms-mapserver-headers - services: - - kind: Service - name: custom-mapfile-wms-mapserver - port: 9111 diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml deleted file mode 100644 index 8537578..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml deleted file mode 100644 index ee088dc..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/service.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/service.yaml deleted file mode 100644 index 2afea5a..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/expected/service.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: custom-mapfile-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: custom-mapfile - uid: "" - blockOwnerDeletion: true - controller: true -spec: - type: ClusterIP - internalTrafficPolicy: Cluster - sessionAffinity: None - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: ogc-webservice-proxy - port: 9111 - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml b/internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml deleted file mode 100644 index 8025b4a..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wms: - contactInformation: - contactPersonPrimary: - contactPerson: KlantContactCenter PDOK - contactOrganization: PDOK - contactPosition: pointOfContact - contactAddress: - addressType: - address: - city: Apeldoorn - stateOrProvince: - postCode: - country: Netherlands - contactVoiceTelephone: - contactFacsimileTelephone: - contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/custom-mapfile/input/wms.yaml b/internal/controller/test_data/wms/custom-mapfile/input/wms.yaml deleted file mode 100644 index 5e933c7..0000000 --- a/internal/controller/test_data/wms/custom-mapfile/input/wms.yaml +++ /dev/null @@ -1,126 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wms - service-version: v1_0 - name: custom-mapfile - namespace: default -spec: - options: {} - podSpecPatch: - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - resources: - limits: - ephemeral-storage: 100m - service: - prefix: dataset - abstract: service-abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - keywords: - - service-keyword - layer: - abstract: service-abstract - keywords: - - service-keyword - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - layers: - - abstract: layer-abstract - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: layer-name - styles: - - name: layer-style-name - title: layer-style-title - title: layer-title - visible: true - - abstract: group - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - layers: - - abstract: group-child - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: group-child - styles: - - name: group-child - title: group-child - - name: style - title: style - title: group-child - visible: true - keywords: - - layer-keyword - name: group - title: group - visible: true - styles: - - name: style - title: style - title: service-title - visible: true - mapfile: - configMapKeyRef: - name: configMap - key: mapfile.map - ownerInfoRef: owner - title: service-title - url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index bfb9ced..0000000 --- a/internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,150 +0,0 @@ ---- -apiVersion: v1 -data: - input.yaml: >- - global: - additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd - http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd - namespace: http://dataset.geonovum.nl - onlineresourceurl: http://localhost - path: /datasetOwner/dataset - prefix: dataset - version: v1_0 - services: - wms130: - definition: - capability: - wmscapabilities: - layer: - - abstract: service-abstract - keywordlist: - keyword: - - service-keyword - layer: - - abstract: layer-abstract - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - layer-keyword - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: layer-name - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: layer-style-name - title: layer-style-title - title: layer-title - - abstract: group - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - layer: - - abstract: group-child - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group-child - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group-child - title: group-child - title: group-child - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group - title: group - title: group - queryable: 1 - title: service-title - service: - abstract: service-abstract - accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - keywordlist: - keyword: - - service-keyword - optionalconstraints: - maxheight: 4000 - maxwidth: 4000 - title: service-title - filename: /var/www/config/capabilities_wms_130.xml -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-capabilities-generator-865bt77thd - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml deleted file mode 100644 index c1ed8a3..0000000 --- a/internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "Projection": "EPSG:28992", - "AutomaticCasing": false, - "Version": 2, - "Layers": [ - { - "Name": "layer-name", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - }, - { - "Name": "group-child", - "GroupName": "group", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-featureinfo-generator-668mmh48cc - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml deleted file mode 100644 index 8409927..0000000 --- a/internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml +++ /dev/null @@ -1,189 +0,0 @@ ---- -apiVersion: v1 -data: - gpkg_download.sh: |- - #!/usr/bin/env bash - - set -euo pipefail - - function download_gpkg() { - local gpkg=$1 - local file=$2 - local url=$3 - - if [ -f "$file" ] && [ ! -f "$file".st ]; then - echo msg=\"File already downloaded\" file=\""$file"\" - else - echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" - - # use curl to check if resource exists - # axel blocks on non-existing resources - curl -IfsS "$url" > /dev/null - - echo start "$gpkg" - ret=0 - # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. - axel -n 1 -T 120 -o "$file" "$url" \ - | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ - | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? - - if [ $ret -ne 0 ] - then - echo -e '\n' - # Download failed ($? != 0). - if [ $ret -eq 1 ] - then - # Axel was not able to resume ($? == 1). Remove file and state file. - if [ -f "$file" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file"\" - rm "$file" - fi - if [ -f "$file.st" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file".st\" - rm "$file".st - fi - else - # Download failed with other error ($? > 1). Remove file if state file does not exist. - if [ ! -f "$file.st" ]; then - echo msg=\"Download failed without state file, removing file\" file=\""$file"\" - rm "$file" - fi - fi - - # Retry the download - echo msg=\"Retry file\" file=\""$file"\" - download_gpkg $gpkg $file $url - fi - fi - } - - function download() { - if [ -z "$BLOBS_ENDPOINT" ]; - then - echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; - exit 1; - fi - - local gpkg=$1 - local file=/srv/data/gpkg/$2 - local url=${BLOBS_ENDPOINT}/${gpkg} - - download_gpkg $gpkg $file $url - - # Check Content-length - download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') - file_size=$(wc -c "$file" | awk '{print $1}') - if [ "$download_size" != "$file_size" ] - then - echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - rm_file_and_exit - else - echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - chown 999:999 "$file" - fi - - # Check ogrinfo - echo "Check gpkg with ogrinfo" - if ! ogrinfo -so "$file" - then - echo "ERROR: ogrinfo check on $file failed" - rm_file_and_exit - fi - - # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) - echo "Check if md5 hash value exists in blob storage" - rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" - - # If file contains valid hash, then check it, else skip - hash=$(awk '{ print $1 }' "${file}.md5sum-remote") - if [[ $hash =~ ^[a-f0-9]{32}$ ]] - then - echo "Valid hash value found" - echo "Compare MD5 hash of remote and downloaded gpkg" - if ! (echo "$hash $file" | md5sum --check); then - rm_file_and_exit - fi - else - echo "No hash found for $file in blob storage, skipping checksum." - fi - - echo "done" - } - - function download_all() { - echo msg=\"Starting GeoPackage downloader\" - - local start_time=$(date '+%s') - - # create target location if not exists - mkdir -p /srv/data/gpkg - chown 999:999 /srv/data/gpkg - - download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; - - echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) - } - - function rm_file_and_exit() { - echo "Removing $file, to ensure a fresh new download is started when script is executed again" - rm -rf "$file" - - if [ -f "$file.st" ]; then - rm "$file".st - fi - - echo "Exiting..." - exit 1 - } - - download_all | awk -W interactive ' - BEGIN { - state="idle"; - } - - { - if ($0 != "") { - if ($1 == "start") { - gpkg=$2; - state="downloading"; - } else if ($1 == "done") { - state="idle"; - } else if (state == "downloading") { - if ($1 == "progress") { - # reduce output to prevent loki from choking on large log volume - if (last_percentage != $2) { - if ($3 == "") { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; - } else { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; - } - } - last_percentage=$2; - } else { - print "msg=\"" $0 "\" gpkg=" gpkg; - } - } else { - print $0; - } - } - } - ' -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-init-scripts-f8k8ffgmgh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml deleted file mode 100644 index e3f270a..0000000 --- a/internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -data: - default_mapserver.conf: ... - input: |- - "layer-name" "layer-style-name" - "group" "group" - "group-child" "group-child" -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-legend-generator-82hh8mg962 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml deleted file mode 100644 index 8723813..0000000 --- a/internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "service_title": "service-title", - "service_abstract": "service-abstract", - "service_keywords": "service-keyword", - "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-25000 250000 280000 860000", - "maxSize": "4000", - "service_namespace_prefix": "dataset", - "service_namespace_uri": "http://dataset.geonovum.nl", - "service_onlineresource": "http://localhost", - "service_path": "/datasetOwner/dataset/wms/v1_0", - "service_metadata_id": "metameta-meta-meta-meta-metametameta", - "dataset_owner": "authority-name", - "authority_url": "http://authority-url", - "automatic_casing": false, - "data_epsg": "EPSG:28992", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "templates": "/srv/data/config/templates", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "symbols": [], - "group_layers": [ - { - "name": "group", - "title": "group", - "abstract": "group", - "style_name": "group", - "style_title": "group" - } - ], - "layers": [ - { - "name": "layer-name", - "gpkg_path": "/srv/data/gpkg/file.gpkg", - "tablename": "layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "layer-column" - } - ], - "title": "layer-title", - "abstract": "layer-abstract", - "keywords": "layer-keyword", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "layer_extent": "-25000 250000 280000 860000", - "styles": [ - { - "title": "layer-style-title", - "path": "/styling/layer-style.style" - } - ] - }, - { - "name": "group-child", - "group_name": "group", - "gpkg_path": "/srv/data/gpkg/file.gpkg", - "tablename": "layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "layer-column" - } - ], - "title": "group-child", - "abstract": "group-child", - "keywords": "", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "src-md-id", - "layer_extent": "-25000 250000 280000 860000", - "styles": [ - { - "title": "group-child", - "path": "/styling/layer-style.style" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapfile-generator-2t677hd4f7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml deleted file mode 100644 index c314789..0000000 --- a/internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,146 +0,0 @@ ---- -apiVersion: v1 -data: - default_mapserver.conf: |- - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - MAPS - MAP "/srv/data/config/mapfile/service.map" - END - END - include.conf: >- - server.modules += ( "mod_status" ) - - - $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { - status.status-url = "/server-status" - } - - - url.rewrite-once = ( - "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", - "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" - ) - - - magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) - - - setenv.add-environment += ( - "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, - ) - ogc.lua: >- - if lighty.r.req_attr["request.method"] == "GET" then - - -- obtain service type from environment - serviceType = os.getenv('SERVICE_TYPE'):lower() - - path = lighty.r.req_attr["uri.path"] - query = lighty.r.req_attr["uri.query"] - - -- handle legend requests - if serviceType == "wms" then - _, _, file = path:find(".*/legend/(.*)") - if file then - if file:find(".*%.png") then - local legendPath = "/var/www/legend/" .. file - local stat = lighty.stat(legendPath) - if (not stat or not stat.is_file) then - -- don't serve non existing legend file - return 404 - end - lighty.content = { { filename = legendPath } } - lighty.header['Content-Type'] = "image/png" - return 200 - end - - return 404 - end - end - - params = {} - if query then - for k, v in query:gmatch("([^?&=]+)=([^&]+)") do - k = k:lower() - - params[k] = v - end - end - - -- assign service and version default values - version = params['version'] - service = params['service'] - - if not service then - service = serviceType - else - service = service:lower() - end - - if (service == 'wms' and (not version or version ~= '1.1.1')) then - version = '1.3.0' - end - - if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then - version = '2.0.0' - end - - -- serve static content - request = params['request'] - if request then - request = request:lower() - - staticStatus = 200 - staticContentType = 'text/xml; charset=UTF-8' - if request == 'getcapabilities' then - if (service == 'wms' and version == '1.3.0') then - staticFile = '/var/www/config/capabilities_wms_130.xml' - elseif (service == 'wfs' and version == '2.0.0') then - staticFile = '/var/www/config/capabilities_wfs_200.xml' - end - elseif service == 'wfs' and request == 'getfeature' then - startindex = params['startindex'] - if startindex and tonumber(startindex) > 50000 then - staticFile = '/srv/mapserver/config/scraping-error.xml' - staticStatus = 400 - end - end - - if staticFile then - lighty.content = { { filename = staticFile } } - lighty.header['Content-Type'] = staticContentType - return staticStatus - end - end - end - scraping-error.xml: >- - - - - - - It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. - - - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapserver-df94mb2d76 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml deleted file mode 100644 index f316d4a..0000000 --- a/internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: v1 -data: - service-config.yaml: |- - grouplayers: - group-layer-name: - - gpkg-layer-name - - postgis-layer-name - top-layer-name: - - gpkg-layer-name - - postgis-layer-name - - tif-layer-name - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wms - service-version: v1_0 - name: minimal-wms-ogc-webservice-proxy-22tb5878f7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/deployment.yaml b/internal/controller/test_data/wms/minimal/expected/deployment.yaml deleted file mode 100644 index 37bfbd2..0000000 --- a/internal/controller/test_data/wms/minimal/expected/deployment.yaml +++ /dev/null @@ -1,364 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: "9117" - prometheus.io/scrape: "true" - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - spec: - restartPolicy: Always - dnsPolicy: ClusterFirst - containers: - - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - - name: SERVICE_TYPE - value: WMS - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - lifecycle: - preStop: - exec: - command: - - sleep - - "15" - livenessProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabil\ - ities' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: text/xml'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - name: mapserver - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png' - 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: image/png'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - resources: - limits: - ephemeral-storage: 200M - memory: 800M - requests: - cpu: "0.1" - startupProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name,group,group-child&STYLES=&FORMAT=image/png' - 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: image/png'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - args: - - --scrape_uri=http://localhost/server-status?auto - image: test.test/image:test7 - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: "0.02" - - name: ogc-webservice-proxy - image: test.test/image:test6 - imagePullPolicy: IfNotPresent - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - limits: - memory: 200M - requests: - cpu: "0.05" - command: - - /ogc-webservice-proxy - - -h=http://127.0.0.1/ - - -t=wms - - -s=/input/service-config.yaml - - -v - - -d=15 - ports: - - containerPort: 9111 - volumeMounts: - - name: ogc-webservice-proxy-config - mountPath: /input - readOnly: true - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - bash /srv/scripts/gpkg_download.sh; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - requests: - cpu: '0.15' - limits: - cpu: '0.2' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - mountPath: /srv/scripts - name: init-scripts - readOnly: true - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:test4 - imagePullPolicy: IfNotPresent - name: capabilities-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --not-include - - wms - - /input/input.json - - /srv/data/config/mapfile - command: - - generate-mapfile - image: test.test/image:test2 - imagePullPolicy: IfNotPresent - name: mapfile-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: mapfile-generator-config - readOnly: true - - mountPath: /styling - name: styling-files - readOnly: true - - args: - - --input-path - - /input/input.json - - --dest-folder - - /srv/data/config/templates - - --file-name - - feature-info - command: - - featureinfo-generator - image: test.test/image:test5 - imagePullPolicy: IfNotPresent - name: featureinfo-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: featureinfo-generator-config - readOnly: true - - command: - - bash - - -c - - | - set -eu; - exit_code=0; - cat /input/input | xargs -n 2 echo | while read layer style; do - echo Generating legend for layer: $layer, style: $style; - mkdir -p /var/www/legend/$layer; - mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; - magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); - if [[ $magic_bytes != 'PNG' ]]; then - echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; - exit_code=1; - fi; - done; - exit $exit_code; - env: - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - name: legend-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/default_mapserver.conf - name: mapserver - subPath: default_mapserver.conf - - mountPath: /input - name: legend-generator-config - readOnly: true - terminationGracePeriodSeconds: 60 - volumes: - - emptyDir: {} - name: base - - emptyDir: {} - name: data - - configMap: - name: minimal-wms-mapserver-df94mb2d76 - defaultMode: 420 - name: mapserver - - configMap: - name: minimal-wms-ogc-webservice-proxy-22tb5878f7 - defaultMode: 420 - name: ogc-webservice-proxy-config - - configMap: - defaultMode: 511 - name: minimal-wms-init-scripts-f8k8ffgmgh - name: init-scripts - - configMap: - name: minimal-wms-capabilities-generator-865bt77thd - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: minimal-wms-mapfile-generator-2t677hd4f7 - defaultMode: 420 - name: mapfile-generator-config - - name: styling-files - projected: - sources: - - configMap: - name: styling - - configMap: - name: minimal-wms-featureinfo-generator-668mmh48cc - defaultMode: 420 - name: featureinfo-generator-config - - configMap: - name: minimal-wms-legend-generator-82hh8mg962 - defaultMode: 420 - name: legend-generator-config diff --git a/internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index fbe9c3e..0000000 --- a/internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,52 +0,0 @@ ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 600 - type: Percent - value: 10 - - periodSeconds: 600 - type: Pods - value: 1 - selectPolicy: Max - stabilizationWindowSeconds: 3600 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 0 - maxReplicas: 30 - metrics: - - resource: - name: cpu - target: - averageUtilization: 90 - type: Utilization - type: Resource - minReplicas: 2 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: minimal-wms-mapserver diff --git a/internal/controller/test_data/wms/minimal/expected/ingressroute.yaml b/internal/controller/test_data/wms/minimal/expected/ingressroute.yaml deleted file mode 100644 index b5d8919..0000000 --- a/internal/controller/test_data/wms/minimal/expected/ingressroute.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true - annotations: - "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" - uptime.pdok.nl/id: 6b32f83fa679db692793ba30367d286b3de46f8a - uptime.pdok.nl/name: MINIMAL WMS - uptime.pdok.nl/tags: public-stats,wms - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png -spec: - routes: - - kind: Rule - match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) - middlewares: - - name: minimal-wms-mapserver-headers - services: - - kind: Service - name: minimal-wms-mapserver - port: 80 - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) - middlewares: - - name: minimal-wms-mapserver-headers - services: - - kind: Service - name: minimal-wms-mapserver - port: 9111 diff --git a/internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml b/internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml deleted file mode 100644 index f6642f1..0000000 --- a/internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml deleted file mode 100644 index 8a5fe6d..0000000 --- a/internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/minimal/expected/service.yaml b/internal/controller/test_data/wms/minimal/expected/service.yaml deleted file mode 100644 index 33b6fc8..0000000 --- a/internal/controller/test_data/wms/minimal/expected/service.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: minimal-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: minimal - uid: "" - blockOwnerDeletion: true - controller: true -spec: - type: ClusterIP - internalTrafficPolicy: Cluster - sessionAffinity: None - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: ogc-webservice-proxy - port: 9111 - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/minimal/input/ownerinfo.yaml b/internal/controller/test_data/wms/minimal/input/ownerinfo.yaml deleted file mode 100644 index 8025b4a..0000000 --- a/internal/controller/test_data/wms/minimal/input/ownerinfo.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wms: - contactInformation: - contactPersonPrimary: - contactPerson: KlantContactCenter PDOK - contactOrganization: PDOK - contactPosition: pointOfContact - contactAddress: - addressType: - address: - city: Apeldoorn - stateOrProvince: - postCode: - country: Netherlands - contactVoiceTelephone: - contactFacsimileTelephone: - contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/minimal/input/wms.yaml b/internal/controller/test_data/wms/minimal/input/wms.yaml deleted file mode 100644 index a70075d..0000000 --- a/internal/controller/test_data/wms/minimal/input/wms.yaml +++ /dev/null @@ -1,131 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wms - service-version: v1_0 - name: minimal - namespace: default -spec: - options: {} - podSpecPatch: - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - resources: - limits: - ephemeral-storage: 100m - service: - prefix: dataset - abstract: service-abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - keywords: - - service-keyword - layer: - abstract: service-abstract - keywords: - - service-keyword - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - layers: - - abstract: layer-abstract - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: layer-name - styles: - - name: layer-style-name - title: layer-style-title - visualization: layer-style.style - title: layer-title - visible: true - - abstract: group - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - layers: - - abstract: group-child - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: group-child - styles: - - name: group-child - title: group-child - visualization: layer-style.style - - name: style - title: style - visualization: style.style - title: group-child - visible: true - keywords: - - layer-keyword - name: group - title: group - visible: true - styles: - - name: style - title: style - title: service-title - visible: true - ownerInfoRef: owner - stylingAssets: - configMapRefs: - - keys: - - layer-style.style - - style.style - name: styling - title: service-title - url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index 662b03d..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,150 +0,0 @@ ---- -apiVersion: v1 -data: - input.yaml: >- - global: - additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd - http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd - namespace: http://dataset.geonovum.nl - onlineresourceurl: http://localhost - path: /datasetOwner/dataset - prefix: dataset - version: v1_0 - services: - wms130: - definition: - capability: - wmscapabilities: - layer: - - abstract: service-abstract - keywordlist: - keyword: - - service-keyword - layer: - - abstract: layer-abstract - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - layer-keyword - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: layer-name - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: layer-style-name - title: layer-style-title - title: layer-title - - abstract: group - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - layer: - - abstract: group-child - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group-child - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group-child - title: group-child - title: group-child - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group - title: group - title: group - queryable: 1 - title: service-title - service: - abstract: service-abstract - accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - keywordlist: - keyword: - - service-keyword - optionalconstraints: - maxheight: 4000 - maxwidth: 4000 - title: service-title - filename: /var/www/config/capabilities_wms_130.xml -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-capabilities-generator-865bt77thd - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml deleted file mode 100644 index 28a5456..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "Projection": "EPSG:28992", - "AutomaticCasing": false, - "Version": 2, - "Layers": [ - { - "Name": "layer-name", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - }, - { - "Name": "group-child", - "GroupName": "group", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-featureinfo-generator-668mmh48cc - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml deleted file mode 100644 index 42c8d9d..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -data: - default_mapserver.conf: ... - input: |- - "layer-name" "layer-style-name" - "group" "group" - "group-child" "group-child" -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-legend-generator-82hh8mg962 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml deleted file mode 100644 index 5b199d8..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "service_title": "service-title", - "service_abstract": "service-abstract", - "service_keywords": "service-keyword", - "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-25000 250000 280000 860000", - "maxSize": "4000", - "service_namespace_prefix": "dataset", - "service_namespace_uri": "http://dataset.geonovum.nl", - "service_onlineresource": "http://localhost", - "service_path": "/datasetOwner/dataset/wms/v1_0", - "service_metadata_id": "metameta-meta-meta-meta-metametameta", - "dataset_owner": "authority-name", - "authority_url": "http://authority-url", - "automatic_casing": false, - "data_epsg": "EPSG:28992", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "templates": "/srv/data/config/templates", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "symbols": [], - "group_layers": [ - { - "name": "group", - "title": "group", - "abstract": "group", - "style_name": "group", - "style_title": "group" - } - ], - "layers": [ - { - "name": "layer-name", - "gpkg_path": "/srv/data/gpkg/file.gpkg", - "tablename": "layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "layer-column" - } - ], - "title": "layer-title", - "abstract": "layer-abstract", - "keywords": "layer-keyword", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "layer_extent": "-25000 250000 280000 860000", - "styles": [ - { - "title": "layer-style-title", - "path": "/styling/layer-style.style" - } - ] - }, - { - "name": "group-child", - "group_name": "group", - "gpkg_path": "/srv/data/gpkg/file.gpkg", - "tablename": "layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "layer-column" - } - ], - "title": "group-child", - "abstract": "group-child", - "keywords": "", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "src-md-id", - "layer_extent": "-25000 250000 280000 860000", - "styles": [ - { - "title": "group-child", - "path": "/styling/layer-style.style" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapfile-generator-bcd2255b7k - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml deleted file mode 100644 index 5c69f72..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,146 +0,0 @@ ---- -apiVersion: v1 -data: - default_mapserver.conf: |- - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - MAPS - MAP "/srv/data/config/mapfile/service.map" - END - END - include.conf: >- - server.modules += ( "mod_status" ) - - - $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { - status.status-url = "/server-status" - } - - - url.rewrite-once = ( - "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", - "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" - ) - - - magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) - - - setenv.add-environment += ( - "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, - ) - ogc.lua: >- - if lighty.r.req_attr["request.method"] == "GET" then - - -- obtain service type from environment - serviceType = os.getenv('SERVICE_TYPE'):lower() - - path = lighty.r.req_attr["uri.path"] - query = lighty.r.req_attr["uri.query"] - - -- handle legend requests - if serviceType == "wms" then - _, _, file = path:find(".*/legend/(.*)") - if file then - if file:find(".*%.png") then - local legendPath = "/var/www/legend/" .. file - local stat = lighty.stat(legendPath) - if (not stat or not stat.is_file) then - -- don't serve non existing legend file - return 404 - end - lighty.content = { { filename = legendPath } } - lighty.header['Content-Type'] = "image/png" - return 200 - end - - return 404 - end - end - - params = {} - if query then - for k, v in query:gmatch("([^?&=]+)=([^&]+)") do - k = k:lower() - - params[k] = v - end - end - - -- assign service and version default values - version = params['version'] - service = params['service'] - - if not service then - service = serviceType - else - service = service:lower() - end - - if (service == 'wms' and (not version or version ~= '1.1.1')) then - version = '1.3.0' - end - - if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then - version = '2.0.0' - end - - -- serve static content - request = params['request'] - if request then - request = request:lower() - - staticStatus = 200 - staticContentType = 'text/xml; charset=UTF-8' - if request == 'getcapabilities' then - if (service == 'wms' and version == '1.3.0') then - staticFile = '/var/www/config/capabilities_wms_130.xml' - elseif (service == 'wfs' and version == '2.0.0') then - staticFile = '/var/www/config/capabilities_wfs_200.xml' - end - elseif service == 'wfs' and request == 'getfeature' then - startindex = params['startindex'] - if startindex and tonumber(startindex) > 50000 then - staticFile = '/srv/mapserver/config/scraping-error.xml' - staticStatus = 400 - end - end - - if staticFile then - lighty.content = { { filename = staticFile } } - lighty.header['Content-Type'] = staticContentType - return staticStatus - end - end - end - scraping-error.xml: >- - - - - - - It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. - - - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapserver-df94mb2d76 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml deleted file mode 100644 index 93e5711..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: v1 -data: - service-config.yaml: |- - grouplayers: - group-layer-name: - - gpkg-layer-name - - postgis-layer-name - top-layer-name: - - gpkg-layer-name - - postgis-layer-name - - tif-layer-name - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wms - service-version: v1_0 - name: noprefetch-wms-ogc-webservice-proxy-22tb5878f7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/deployment.yaml b/internal/controller/test_data/wms/noprefetch/expected/deployment.yaml deleted file mode 100644 index b3f1718..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/deployment.yaml +++ /dev/null @@ -1,356 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: "9117" - prometheus.io/scrape: "true" - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - spec: - restartPolicy: Always - dnsPolicy: ClusterFirst - containers: - - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - - name: SERVICE_TYPE - value: WMS - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - lifecycle: - preStop: - exec: - command: - - sleep - - "15" - livenessProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabil\ - ities' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: text/xml'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - name: mapserver - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png' - 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: image/png'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - resources: - limits: - ephemeral-storage: 200M - memory: 800M - requests: - cpu: "0.1" - startupProbe: - exec: - command: - - /bin/sh - - -c - - "wget -SO- -T 10 -t 2 - 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name,group,group-child&STYLES=&FORMAT=image/png' - 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i - 'Content-Type: image/png'" - successThreshold: 1 - failureThreshold: 3 - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 10 - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - args: - - --scrape_uri=http://localhost/server-status?auto - image: test.test/image:test7 - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: "0.02" - - name: ogc-webservice-proxy - image: test.test/image:test6 - imagePullPolicy: IfNotPresent - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - limits: - memory: 200M - requests: - cpu: "0.05" - command: - - /ogc-webservice-proxy - - -h=http://127.0.0.1/ - - -t=wms - - -s=/input/service-config.yaml - - -v - - -d=15 - ports: - - containerPort: 9111 - volumeMounts: - - name: ogc-webservice-proxy-config - mountPath: /input - readOnly: true - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - requests: - cpu: '0.15' - limits: - cpu: '0.2' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:test4 - imagePullPolicy: IfNotPresent - name: capabilities-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --not-include - - wms - - /input/input.json - - /srv/data/config/mapfile - command: - - generate-mapfile - image: test.test/image:test2 - imagePullPolicy: IfNotPresent - name: mapfile-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: mapfile-generator-config - readOnly: true - - mountPath: /styling - name: styling-files - readOnly: true - - args: - - --input-path - - /input/input.json - - --dest-folder - - /srv/data/config/templates - - --file-name - - feature-info - command: - - featureinfo-generator - image: test.test/image:test5 - imagePullPolicy: IfNotPresent - name: featureinfo-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: featureinfo-generator-config - readOnly: true - - command: - - bash - - -c - - | - set -eu; - exit_code=0; - cat /input/input | xargs -n 2 echo | while read layer style; do - echo Generating legend for layer: $layer, style: $style; - mkdir -p /var/www/legend/$layer; - mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; - magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); - if [[ $magic_bytes != 'PNG' ]]; then - echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; - exit_code=1; - fi; - done; - exit $exit_code; - env: - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:test3 - imagePullPolicy: IfNotPresent - name: legend-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/default_mapserver.conf - name: mapserver - subPath: default_mapserver.conf - - mountPath: /input - name: legend-generator-config - readOnly: true - terminationGracePeriodSeconds: 60 - volumes: - - emptyDir: {} - name: base - - emptyDir: {} - name: data - - configMap: - name: noprefetch-wms-mapserver-df94mb2d76 - defaultMode: 420 - name: mapserver - - configMap: - name: noprefetch-wms-ogc-webservice-proxy-22tb5878f7 - defaultMode: 420 - name: ogc-webservice-proxy-config - - configMap: - name: noprefetch-wms-capabilities-generator-865bt77thd - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: noprefetch-wms-mapfile-generator-bcd2255b7k - defaultMode: 420 - name: mapfile-generator-config - - name: styling-files - projected: - sources: - - configMap: - name: styling - - configMap: - name: noprefetch-wms-featureinfo-generator-668mmh48cc - defaultMode: 420 - name: featureinfo-generator-config - - configMap: - name: noprefetch-wms-legend-generator-82hh8mg962 - defaultMode: 420 - name: legend-generator-config diff --git a/internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index d136a5a..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,52 +0,0 @@ ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 600 - type: Percent - value: 10 - - periodSeconds: 600 - type: Pods - value: 1 - selectPolicy: Max - stabilizationWindowSeconds: 3600 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 0 - maxReplicas: 30 - metrics: - - resource: - name: cpu - target: - averageUtilization: 90 - type: Utilization - type: Resource - minReplicas: 2 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: noprefetch-wms-mapserver diff --git a/internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml b/internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml deleted file mode 100644 index feff2ba..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true - annotations: - "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" - uptime.pdok.nl/id: b05a258104b33de6117b1744cd2b8d2231402508 - uptime.pdok.nl/name: NOPREFETCH WMS - uptime.pdok.nl/tags: public-stats,wms - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png -spec: - routes: - - kind: Rule - match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) - middlewares: - - name: noprefetch-wms-mapserver-headers - services: - - kind: Service - name: noprefetch-wms-mapserver - port: 80 - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) - middlewares: - - name: noprefetch-wms-mapserver-headers - services: - - kind: Service - name: noprefetch-wms-mapserver - port: 9111 diff --git a/internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml b/internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml deleted file mode 100644 index 9f2ab12..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml deleted file mode 100644 index e5bc079..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/noprefetch/expected/service.yaml b/internal/controller/test_data/wms/noprefetch/expected/service.yaml deleted file mode 100644 index 1e4e448..0000000 --- a/internal/controller/test_data/wms/noprefetch/expected/service.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: noprefetch-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: noprefetch - uid: "" - blockOwnerDeletion: true - controller: true -spec: - type: ClusterIP - internalTrafficPolicy: Cluster - sessionAffinity: None - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: ogc-webservice-proxy - port: 9111 - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml b/internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml deleted file mode 100644 index 8025b4a..0000000 --- a/internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wms: - contactInformation: - contactPersonPrimary: - contactPerson: KlantContactCenter PDOK - contactOrganization: PDOK - contactPosition: pointOfContact - contactAddress: - addressType: - address: - city: Apeldoorn - stateOrProvince: - postCode: - country: Netherlands - contactVoiceTelephone: - contactFacsimileTelephone: - contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/noprefetch/input/wms.yaml b/internal/controller/test_data/wms/noprefetch/input/wms.yaml deleted file mode 100644 index de21dca..0000000 --- a/internal/controller/test_data/wms/noprefetch/input/wms.yaml +++ /dev/null @@ -1,132 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wms - service-version: v1_0 - name: noprefetch - namespace: default -spec: - options: - prefetchData: false - podSpecPatch: - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - containers: - - name: mapserver - env: - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: blobs-testtest - resources: - limits: - ephemeral-storage: 100m - service: - prefix: dataset - abstract: service-abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - keywords: - - service-keyword - layer: - abstract: service-abstract - keywords: - - service-keyword - boundingBoxes: - - bbox: - maxx: "280000" - maxy: "860000" - minx: "-25000" - miny: "250000" - crs: EPSG:28992 - layers: - - abstract: layer-abstract - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: layer-name - styles: - - name: layer-style-name - title: layer-style-title - visualization: layer-style.style - title: layer-title - visible: true - - abstract: group - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - layers: - - abstract: group-child - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: group-child - styles: - - name: group-child - title: group-child - visualization: layer-style.style - - name: style - title: style - visualization: style.style - title: group-child - visible: true - keywords: - - layer-keyword - name: group - title: group - visible: true - styles: - - name: style - title: style - title: service-title - visible: true - ownerInfoRef: owner - stylingAssets: - configMapRefs: - - keys: - - layer-style.style - - style.style - name: styling - title: service-title - url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml deleted file mode 100644 index 8aff4c3..0000000 --- a/internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml +++ /dev/null @@ -1,150 +0,0 @@ ---- -apiVersion: v1 -data: - input.yaml: >- - global: - additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd - http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd - namespace: http://dataset.geonovum.nl - onlineresourceurl: http://localhost - path: /datasetOwner/dataset - prefix: dataset - version: v1_0 - services: - wms130: - definition: - capability: - wmscapabilities: - layer: - - abstract: service-abstract - keywordlist: - keyword: - - service-keyword - layer: - - abstract: layer-abstract - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: bronbron-bron-bron-bron-bronbronbron - keywordlist: - keyword: - - layer-keyword - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: layer-name - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: layer-style-name - title: layer-style-title - title: layer-title - - abstract: group - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - layer: - - abstract: group-child - authorityurl: - name: authority-name - onlineresource: - href: http://authority-url - identifier: - authority: authority-name - value: src-md-id - keywordlist: - keyword: [] - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group-child - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group-child - title: group-child - title: group-child - metadataurl: - - format: text/plain - onlineresource: - href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata - type: simple - xlink: http://www.w3.org/1999/xlink - type: TC211 - name: group - queryable: 1 - style: - - legendurl: - format: image/png - height: 20 - onlineresource: - href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png - type: simple - xlink: http://www.w3.org/1999/xlink - width: 78 - name: group - title: group - title: group - queryable: 1 - title: service-title - service: - abstract: service-abstract - accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - keywordlist: - keyword: - - service-keyword - optionalconstraints: - maxheight: 4000 - maxwidth: 4000 - title: service-title - filename: /var/www/config/capabilities_wms_130.xml -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-capabilities-generator-f82hgmbht2 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml deleted file mode 100644 index 99c6982..0000000 --- a/internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "Projection": "EPSG:28992", - "AutomaticCasing": false, - "Version": 2, - "Layers": [ - { - "Name": "layer-name", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - }, - { - "Name": "group-child", - "GroupName": "group", - "Properties": [ - { - "Name": "fuuid" - }, - { - "Name": "layer-column" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-featureinfo-generator-668mmh48cc - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml deleted file mode 100644 index f173138..0000000 --- a/internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml +++ /dev/null @@ -1,189 +0,0 @@ ---- -apiVersion: v1 -data: - gpkg_download.sh: |- - #!/usr/bin/env bash - - set -euo pipefail - - function download_gpkg() { - local gpkg=$1 - local file=$2 - local url=$3 - - if [ -f "$file" ] && [ ! -f "$file".st ]; then - echo msg=\"File already downloaded\" file=\""$file"\" - else - echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" - - # use curl to check if resource exists - # axel blocks on non-existing resources - curl -IfsS "$url" > /dev/null - - echo start "$gpkg" - ret=0 - # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. - axel -n 1 -T 120 -o "$file" "$url" \ - | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ - | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? - - if [ $ret -ne 0 ] - then - echo -e '\n' - # Download failed ($? != 0). - if [ $ret -eq 1 ] - then - # Axel was not able to resume ($? == 1). Remove file and state file. - if [ -f "$file" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file"\" - rm "$file" - fi - if [ -f "$file.st" ]; then - echo msg=\"Resume failed, removing file\" file=\""$file".st\" - rm "$file".st - fi - else - # Download failed with other error ($? > 1). Remove file if state file does not exist. - if [ ! -f "$file.st" ]; then - echo msg=\"Download failed without state file, removing file\" file=\""$file"\" - rm "$file" - fi - fi - - # Retry the download - echo msg=\"Retry file\" file=\""$file"\" - download_gpkg $gpkg $file $url - fi - fi - } - - function download() { - if [ -z "$BLOBS_ENDPOINT" ]; - then - echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; - exit 1; - fi - - local gpkg=$1 - local file=/srv/data/gpkg/$2 - local url=${BLOBS_ENDPOINT}/${gpkg} - - download_gpkg $gpkg $file $url - - # Check Content-length - download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') - file_size=$(wc -c "$file" | awk '{print $1}') - if [ "$download_size" != "$file_size" ] - then - echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - rm_file_and_exit - else - echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" - chown 999:999 "$file" - fi - - # Check ogrinfo - echo "Check gpkg with ogrinfo" - if ! ogrinfo -so "$file" - then - echo "ERROR: ogrinfo check on $file failed" - rm_file_and_exit - fi - - # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) - echo "Check if md5 hash value exists in blob storage" - rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" - - # If file contains valid hash, then check it, else skip - hash=$(awk '{ print $1 }' "${file}.md5sum-remote") - if [[ $hash =~ ^[a-f0-9]{32}$ ]] - then - echo "Valid hash value found" - echo "Compare MD5 hash of remote and downloaded gpkg" - if ! (echo "$hash $file" | md5sum --check); then - rm_file_and_exit - fi - else - echo "No hash found for $file in blob storage, skipping checksum." - fi - - echo "done" - } - - function download_all() { - echo msg=\"Starting GeoPackage downloader\" - - local start_time=$(date '+%s') - - # create target location if not exists - mkdir -p /srv/data/gpkg - chown 999:999 /srv/data/gpkg - - download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; - - echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) - } - - function rm_file_and_exit() { - echo "Removing $file, to ensure a fresh new download is started when script is executed again" - rm -rf "$file" - - if [ -f "$file.st" ]; then - rm "$file".st - fi - - echo "Exiting..." - exit 1 - } - - download_all | awk -W interactive ' - BEGIN { - state="idle"; - } - - { - if ($0 != "") { - if ($1 == "start") { - gpkg=$2; - state="downloading"; - } else if ($1 == "done") { - state="idle"; - } else if (state == "downloading") { - if ($1 == "progress") { - # reduce output to prevent loki from choking on large log volume - if (last_percentage != $2) { - if ($3 == "") { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; - } else { - print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; - } - } - last_percentage=$2; - } else { - print "msg=\"" $0 "\" gpkg=" gpkg; - } - } else { - print $0; - } - } - } - ' -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-init-scripts-f8k8ffgmgh - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml deleted file mode 100644 index ae437bc..0000000 --- a/internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: v1 -data: - default_mapserver.conf: ... - input: |- - "layer-name" "layer-style-name" - "group" "group" - "group-child" "group-child" -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-legend-generator-6cf9f5k5h5 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml deleted file mode 100644 index 9f1bb0f..0000000 --- a/internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml +++ /dev/null @@ -1,118 +0,0 @@ -apiVersion: v1 -data: - input.json: |- - { - "service_title": "service-title", - "service_abstract": "service-abstract", - "service_keywords": "service-keyword", - "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", - "service_extent": "-25000 250000 280000 860000", - "maxSize": "4000", - "service_namespace_prefix": "dataset", - "service_namespace_uri": "http://dataset.geonovum.nl", - "service_onlineresource": "http://localhost", - "service_path": "/datasetOwner/dataset/wms/v1_0", - "service_metadata_id": "metameta-meta-meta-meta-metametameta", - "dataset_owner": "authority-name", - "authority_url": "http://authority-url", - "automatic_casing": false, - "data_epsg": "EPSG:28992", - "epsg_list": [ - "EPSG:28992", - "EPSG:25831", - "EPSG:25832", - "EPSG:3034", - "EPSG:3035", - "EPSG:3857", - "EPSG:4258", - "EPSG:4326", - "CRS:84" - ], - "templates": "/srv/data/config/templates", - "outputformat_jpg": "jpg", - "outputformat_png8": "png", - "symbols": [], - "group_layers": [ - { - "name": "group", - "title": "group", - "abstract": "group", - "style_name": "group", - "style_title": "group" - } - ], - "layers": [ - { - "name": "layer-name", - "gpkg_path": "/srv/data/gpkg/file.gpkg", - "tablename": "layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "layer-column" - } - ], - "title": "layer-title", - "abstract": "layer-abstract", - "keywords": "layer-keyword", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", - "layer_extent": "-25000 250000 280000 860000", - "styles": [ - { - "title": "layer-style-title", - "path": "/styling/layer-style.style" - } - ] - }, - { - "name": "group-child", - "group_name": "group", - "gpkg_path": "/srv/data/gpkg/file.gpkg", - "tablename": "layer", - "geometry_type": "Point", - "columns": [ - { - "name": "fuuid" - }, - { - "name": "layer-column" - } - ], - "title": "group-child", - "abstract": "group-child", - "keywords": "", - "dataset_metadata_id": "datadata-data-data-data-datadatadata", - "dataset_source_id": "src-md-id", - "layer_extent": "-25000 250000 280000 860000", - "styles": [ - { - "title": "group-child", - "path": "/styling/layer-style.style" - } - ] - } - ] - } -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapfile-generator-mh72kmt774 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml deleted file mode 100644 index 5bc57d6..0000000 --- a/internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml +++ /dev/null @@ -1,146 +0,0 @@ ---- -apiVersion: v1 -data: - default_mapserver.conf: |- - CONFIG - ENV - MS_MAP_NO_PATH "true" - END - MAPS - MAP "/srv/data/config/mapfile/service.map" - END - END - include.conf: >- - server.modules += ( "mod_status" ) - - - $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { - status.status-url = "/server-status" - } - - - url.rewrite-once = ( - "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", - "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" - ) - - - magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) - - - setenv.add-environment += ( - "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, - ) - ogc.lua: >- - if lighty.r.req_attr["request.method"] == "GET" then - - -- obtain service type from environment - serviceType = os.getenv('SERVICE_TYPE'):lower() - - path = lighty.r.req_attr["uri.path"] - query = lighty.r.req_attr["uri.query"] - - -- handle legend requests - if serviceType == "wms" then - _, _, file = path:find(".*/legend/(.*)") - if file then - if file:find(".*%.png") then - local legendPath = "/var/www/legend/" .. file - local stat = lighty.stat(legendPath) - if (not stat or not stat.is_file) then - -- don't serve non existing legend file - return 404 - end - lighty.content = { { filename = legendPath } } - lighty.header['Content-Type'] = "image/png" - return 200 - end - - return 404 - end - end - - params = {} - if query then - for k, v in query:gmatch("([^?&=]+)=([^&]+)") do - k = k:lower() - - params[k] = v - end - end - - -- assign service and version default values - version = params['version'] - service = params['service'] - - if not service then - service = serviceType - else - service = service:lower() - end - - if (service == 'wms' and (not version or version ~= '1.1.1')) then - version = '1.3.0' - end - - if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then - version = '2.0.0' - end - - -- serve static content - request = params['request'] - if request then - request = request:lower() - - staticStatus = 200 - staticContentType = 'text/xml; charset=UTF-8' - if request == 'getcapabilities' then - if (service == 'wms' and version == '1.3.0') then - staticFile = '/var/www/config/capabilities_wms_130.xml' - elseif (service == 'wfs' and version == '2.0.0') then - staticFile = '/var/www/config/capabilities_wfs_200.xml' - end - elseif service == 'wfs' and request == 'getfeature' then - startindex = params['startindex'] - if startindex and tonumber(startindex) > 50000 then - staticFile = '/srv/mapserver/config/scraping-error.xml' - staticStatus = 400 - end - end - - if staticFile then - lighty.content = { { filename = staticFile } } - lighty.header['Content-Type'] = staticContentType - return staticStatus - end - end - end - scraping-error.xml: >- - - - - - - It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. - - - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapserver-df94mb2d76 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml deleted file mode 100644 index e345ad1..0000000 --- a/internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: v1 -data: - service-config.yaml: |- - grouplayers: - group-layer-name: - - gpkg-layer-name - - postgis-layer-name - top-layer-name: - - gpkg-layer-name - - postgis-layer-name - - tif-layer-name - -immutable: true -kind: ConfigMap -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: 'false' - service-type: wms - service-version: v1_0 - name: patches-wms-ogc-webservice-proxy-22tb5878f7 - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true diff --git a/internal/controller/test_data/wms/patches/expected/deployment.yaml b/internal/controller/test_data/wms/patches/expected/deployment.yaml deleted file mode 100644 index 2ecdf4e..0000000 --- a/internal/controller/test_data/wms/patches/expected/deployment.yaml +++ /dev/null @@ -1,372 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true -spec: - revisionHistoryLimit: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - strategy: - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - type: RollingUpdate - template: - metadata: - annotations: - cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' - kubectl.kubernetes.io/default-container: mapserver - match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ - prometheus.io/port: "9117" - prometheus.io/scrape: "true" - priority.version-checker.io/mapserver: "4" - priority.version-checker.io/ogc-webservice-proxy: "4" - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - spec: - restartPolicy: Never - dnsPolicy: None - containers: - - env: - - name: SERVICE_TYPE - value: NONE - - name: MAPSERVER_CONFIG_FILE - value: "/srv/patch/config" - - name: MS_MAPFILE - value: /srv/patch/map.map - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: patch-blobs - - name: PATCH - value: PATCH - image: patch.patch/image:patch - imagePullPolicy: Always - lifecycle: - preStop: - exec: - command: - - patch - - "15" - livenessProbe: - exec: - command: - - /bin/sh - - -c - - "patch" - successThreshold: 10 - failureThreshold: 30 - initialDelaySeconds: 200 - periodSeconds: 100 - timeoutSeconds: 100 - name: mapserver - terminationMessagePath: /patch/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 8000 - protocol: SCTP - - name: patch - containerPort: 5000 - protocol: UDP - hostIP: patch - hostPort: 5050 - - containerPort: 80 - protocol: TCP - readinessProbe: - exec: - command: - - /bin/sh - - -c - - "patch" - successThreshold: 10 - failureThreshold: 30 - initialDelaySeconds: 200 - periodSeconds: 100 - timeoutSeconds: 100 - terminationGracePeriodSeconds: 1000 - resources: - limits: - ephemeral-storage: 4000M - memory: 8000M - cpu: "5" - requests: - cpu: "5" - ephemeral-storage: 2000M - memory: 2000M - startupProbe: - exec: - command: - - /bin/sh - - -c - - "patch" - successThreshold: 10 - failureThreshold: 30 - initialDelaySeconds: 200 - periodSeconds: 100 - timeoutSeconds: 100 - volumeMounts: - - mountPath: /srv/data/patch - name: base - readOnly: false - - mountPath: /var/www/patch - name: data - readOnly: false - subPath: /patch - - name: patch - mountPath: /patch - - name: base - mountPath: /srv/data - readOnly: false - - name: data - mountPath: /var/www - - mountPath: /srv/mapserver/config/include.conf - name: mapserver - subPath: include.conf - - mountPath: /srv/mapserver/config/ogc.lua - name: mapserver - subPath: ogc.lua - - name: mapserver - mountPath: /srv/mapserver/config/default_mapserver.conf - subPath: default_mapserver.conf - - mountPath: /srv/mapserver/config/scraping-error.xml - name: mapserver - subPath: scraping-error.xml - - args: - - "patch" - image: patch.patch/image:patch - imagePullPolicy: IfNotPresent - name: apache-exporter - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 9117 - protocol: TCP - resources: - limits: - memory: 48M - requests: - cpu: "0.02" - - name: ogc-webservice-proxy - image: patch.patch/image:patch - imagePullPolicy: IfNotPresent - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - limits: - memory: 200M - requests: - cpu: "0.05" - command: - - /ogc-webservice-proxy - - -h=http://127.0.0.1/ - - -t=wms - - -s=/input/service-config.yaml - - -v - - -d=15 - ports: - - containerPort: 9111 - volumeMounts: - - name: ogc-webservice-proxy-config - mountPath: /input - readOnly: true - initContainers: - - args: - - | - set -e; - mkdir -p /srv/data/config/; - rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; - bash /srv/scripts/gpkg_download.sh; - command: - - /bin/sh - - -c - env: - - name: GEOPACKAGE_TARGET_PATH - value: /srv/data/gpkg - - name: GEOPACKAGE_DOWNLOAD_LIST - value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - image: test.test/image:test1 - imagePullPolicy: IfNotPresent - name: blob-download - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - resources: - requests: - cpu: '3' - limits: - cpu: '5' - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - name: data - mountPath: /var/www - readOnly: false - - mountPath: /srv/scripts - name: init-scripts - readOnly: true - - env: - - name: SERVICECONFIG - value: /input/input.yaml - image: test.test/image:patch5 - imagePullPolicy: Always - name: capabilities-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /input - name: capabilities-generator-config - readOnly: true - - args: - - --not-include - - wms - - /input/input.json - - /srv/data/config/mapfile - command: - - generate-mapfile - image: test.test/image:patch3 - imagePullPolicy: IfNotPresent - name: mapfile-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: mapfile-generator-config - readOnly: true - - mountPath: /styling - name: patch - readOnly: true - - args: - - --input-path - - /input/input.json - - --dest-folder - - /srv/data/config/templates - - --file-name - - feature-info - command: - - featureinfo-generator - image: test.test/image:patch5 - imagePullPolicy: Always - name: featureinfo-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /input - name: featureinfo-generator-config - readOnly: true - resources: - requests: - memory: '300M' - limits: - memory: '500M' - - command: - - bash - - -c - - | - patch; - env: - - name: MAPSERVER_CONFIG_FILE - value: "/srv/mapserver/config/default_mapserver.conf" - - name: MS_MAPFILE - value: /srv/data/config/mapfile/service.map - image: test.test/image:patch - imagePullPolicy: IfNotPresent - name: legend-generator - terminationMessagePath: /dev/termination-log - terminationMessagePolicy: File - volumeMounts: - - mountPath: /srv/data - name: base - readOnly: false - - mountPath: /var/www - name: data - readOnly: false - - mountPath: /srv/mapserver/config/default_mapserver.conf - name: mapserver - subPath: default_mapserver.conf - - mountPath: /input - name: legend-generator-config - readOnly: true - terminationGracePeriodSeconds: 600 - volumes: - - emptyDir: {} - name: base - - emptyDir: {} - name: data - - configMap: - name: patch - defaultMode: 420 - name: mapserver - - name: patch - csi: - driver: patch - - configMap: - name: patches-wms-ogc-webservice-proxy-22tb5878f7 - defaultMode: 420 - name: ogc-webservice-proxy-config - - configMap: - defaultMode: 511 - name: patches-wms-init-scripts-f8k8ffgmgh - name: init-scripts - - configMap: - name: patches-wms-capabilities-generator-f82hgmbht2 - defaultMode: 420 - name: capabilities-generator-config - - configMap: - name: patches-wms-mapfile-generator-mh72kmt774 - defaultMode: 420 - name: mapfile-generator-config - - name: styling-files - projected: - sources: - - configMap: - name: styling - - configMap: - name: patches-wms-featureinfo-generator-668mmh48cc - defaultMode: 420 - name: featureinfo-generator-config - - configMap: - name: patches-wms-legend-generator-6cf9f5k5h5 - defaultMode: 420 - name: legend-generator-config diff --git a/internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml deleted file mode 100644 index 7afbfaf..0000000 --- a/internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml +++ /dev/null @@ -1,49 +0,0 @@ ---- -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true -spec: - behavior: - scaleDown: - policies: - - periodSeconds: 30 - type: Pods - value: 1 - selectPolicy: Min - stabilizationWindowSeconds: 50 - scaleUp: - policies: - - periodSeconds: 60 - type: Pods - value: 20 - selectPolicy: Max - stabilizationWindowSeconds: 0 - maxReplicas: 10 - metrics: - - resource: - name: memory - target: - averageUtilization: 90 - type: Utilization - type: Resource - minReplicas: 5 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: patches-wms-mapserver diff --git a/internal/controller/test_data/wms/patches/expected/ingressroute.yaml b/internal/controller/test_data/wms/patches/expected/ingressroute.yaml deleted file mode 100644 index fc947f3..0000000 --- a/internal/controller/test_data/wms/patches/expected/ingressroute.yaml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true - annotations: - "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" - uptime.pdok.nl/id: 2f02f7c27fd537fcc52564dab35d9967ff9a0b9d - uptime.pdok.nl/name: PATCHES WMS - uptime.pdok.nl/tags: public-stats,wms - uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png -spec: - routes: - - kind: Rule - match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) - middlewares: - - name: patches-wms-mapserver-headers - services: - - kind: Service - name: patches-wms-mapserver - port: 80 - - kind: Rule - match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) - middlewares: - - name: patches-wms-mapserver-headers - services: - - kind: Service - name: patches-wms-mapserver - port: 9111 diff --git a/internal/controller/test_data/wms/patches/expected/middleware-headers.yaml b/internal/controller/test_data/wms/patches/expected/middleware-headers.yaml deleted file mode 100644 index 0ab983f..0000000 --- a/internal/controller/test_data/wms/patches/expected/middleware-headers.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapserver-headers - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true -spec: - headers: - customResponseHeaders: - Access-Control-Allow-Headers: Content-Type - Access-Control-Allow-Method: GET, POST, OPTIONS - Access-Control-Allow-Origin: '*' - Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml deleted file mode 100644 index 33c3461..0000000 --- a/internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: policy/v1 -kind: PodDisruptionBudget -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true -spec: - maxUnavailable: 1 - selector: - matchLabels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/patches/expected/service.yaml b/internal/controller/test_data/wms/patches/expected/service.yaml deleted file mode 100644 index c873515..0000000 --- a/internal/controller/test_data/wms/patches/expected/service.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -apiVersion: v1 -kind: Service -metadata: - labels: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 - name: patches-wms-mapserver - namespace: default - ownerReferences: - - apiVersion: pdok.nl/v3 - kind: WMS - name: patches - uid: "" - blockOwnerDeletion: true - controller: true -spec: - type: ClusterIP - internalTrafficPolicy: Cluster - sessionAffinity: None - ports: - - name: mapserver - port: 80 - targetPort: 80 - protocol: TCP - - name: ogc-webservice-proxy - port: 9111 - - name: metric - port: 9117 - targetPort: 9117 - protocol: TCP - selector: - pdok.nl/app: mapserver - dataset: dataset - dataset-owner: datasetOwner - pdok.nl/inspire: "false" - service-type: wms - service-version: v1_0 diff --git a/internal/controller/test_data/wms/patches/input/ownerinfo.yaml b/internal/controller/test_data/wms/patches/input/ownerinfo.yaml deleted file mode 100644 index 8025b4a..0000000 --- a/internal/controller/test_data/wms/patches/input/ownerinfo.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wms: - contactInformation: - contactPersonPrimary: - contactPerson: KlantContactCenter PDOK - contactOrganization: PDOK - contactPosition: pointOfContact - contactAddress: - addressType: - address: - city: Apeldoorn - stateOrProvince: - postCode: - country: Netherlands - contactVoiceTelephone: - contactFacsimileTelephone: - contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/patches/input/wms.yaml b/internal/controller/test_data/wms/patches/input/wms.yaml deleted file mode 100644 index e42d579..0000000 --- a/internal/controller/test_data/wms/patches/input/wms.yaml +++ /dev/null @@ -1,255 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - annotations: - pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta - labels: - dataset: dataset - dataset-owner: datasetOwner - service-type: wms - service-version: v1_0 - name: patches - namespace: default -spec: - options: {} - horizontalPodAutoscalerPatch: - behavior: - scaleDown: - policies: - - periodSeconds: 30 - type: Pods - value: 1 - selectPolicy: Min - stabilizationWindowSeconds: 50 - maxReplicas: 10 - metrics: - - resource: - name: memory - target: - averageUtilization: 90 - type: Utilization - type: Resource - minReplicas: 5 - podSpecPatch: - restartPolicy: Never - dnsPolicy: None - containers: - - env: - - name: SERVICE_TYPE - value: NONE - - name: MAPSERVER_CONFIG_FILE - value: "/srv/patch/config" - - name: MS_MAPFILE - value: /srv/patch/map.map - - name: AZURE_STORAGE_CONNECTION_STRING - valueFrom: - secretKeyRef: - key: AZURE_STORAGE_CONNECTION_STRING - name: patch-blobs - - name: PATCH - value: PATCH - image: patch.patch/image:patch - imagePullPolicy: Always - lifecycle: - preStop: - exec: - command: - - patch - - "15" - livenessProbe: - exec: - command: - - /bin/sh - - -c - - "patch" - successThreshold: 10 - failureThreshold: 30 - initialDelaySeconds: 200 - periodSeconds: 100 - timeoutSeconds: 100 - name: mapserver - terminationMessagePath: /patch/termination-log - terminationMessagePolicy: File - ports: - - containerPort: 8000 - protocol: SCTP - - name: patch - containerPort: 5000 - protocol: UDP - hostIP: patch - hostPort: 5050 - readinessProbe: - exec: - command: - - /bin/sh - - -c - - "patch" - successThreshold: 10 - failureThreshold: 30 - initialDelaySeconds: 200 - periodSeconds: 100 - timeoutSeconds: 100 - terminationGracePeriodSeconds: 1000 - resources: - limits: - ephemeral-storage: 4000M - memory: 8000M - cpu: "5" - requests: - cpu: "5" - ephemeral-storage: 2000M - memory: 2000M - startupProbe: - exec: - command: - - /bin/sh - - -c - - "patch" - successThreshold: 10 - failureThreshold: 30 - initialDelaySeconds: 200 - periodSeconds: 100 - timeoutSeconds: 100 - volumeMounts: - - mountPath: /srv/data/patch - name: base - readOnly: false - - mountPath: /var/www/patch - name: data - readOnly: false - subPath: /patch - - name: patch - mountPath: /patch - - args: - - "patch" - image: patch.patch/image:patch - name: apache-exporter - - name: ogc-webservice-proxy - image: patch.patch/image:patch - initContainers: - - name: blob-download - envFrom: - - configMapRef: - name: blobs-testtest - - secretRef: - name: blobs-testtest - resources: - requests: - cpu: '3' - limits: - cpu: '5' - - image: test.test/image:patch5 - imagePullPolicy: Always - name: capabilities-generator - - image: test.test/image:patch3 - name: mapfile-generator - volumeMounts: - - mountPath: /styling - name: patch - readOnly: true - - image: test.test/image:patch5 - imagePullPolicy: Always - name: featureinfo-generator - resources: - requests: - memory: '300M' - limits: - memory: '500M' - - command: - - bash - - -c - - | - patch; - image: test.test/image:patch - name: legend-generator - terminationGracePeriodSeconds: 600 - volumes: - - configMap: - name: patch - name: mapserver - - name: patch - csi: - driver: patch - service: - abstract: service-abstract - accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl - dataEPSG: EPSG:28992 - keywords: - - service-keyword - layer: - abstract: service-abstract - keywords: - - service-keyword - layers: - - abstract: layer-abstract - authority: - name: authority-name - spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: layer-name - styles: - - name: layer-style-name - title: layer-style-title - visualization: layer-style.style - title: layer-title - visible: true - - abstract: group - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - layers: - - abstract: group-child - authority: - name: authority-name - spatialDatasetIdentifier: src-md-id - url: http://authority-url - data: - gpkg: - blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg - columns: - - name: layer-column - geometryType: Point - tableName: layer - datasetMetadataUrl: - csw: - metadataIdentifier: datadata-data-data-data-datadatadata - keywords: - - layer-keyword - name: group-child - styles: - - name: group-child - title: group-child - visualization: layer-style.style - title: group-child - visible: true - keywords: - - layer-keyword - name: group - title: group - visible: true - title: service-title - visible: true - ownerInfoRef: owner - stylingAssets: - configMapRefs: - - keys: - - layer-style.style - name: styling - title: service-title - url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/types/types.go b/internal/controller/types/types.go deleted file mode 100644 index 34e7d03..0000000 --- a/internal/controller/types/types.go +++ /dev/null @@ -1,21 +0,0 @@ -package types - -type HashedConfigMapNames struct { - Mapserver string - InitScripts string - MapfileGenerator string - CapabilitiesGenerator string - OgcWebserviceProxy string - LegendGenerator string - FeatureInfoGenerator string -} - -type Images struct { - MapserverImage string - MultitoolImage string - MapfileGeneratorImage string - CapabilitiesGeneratorImage string - FeatureinfoGeneratorImage string - OgcWebserviceProxyImage string - ApacheExporterImage string -} diff --git a/internal/controller/utils/utils.go b/internal/controller/utils/utils.go deleted file mode 100644 index 37fee7e..0000000 --- a/internal/controller/utils/utils.go +++ /dev/null @@ -1,66 +0,0 @@ -package utils - -import ( - //nolint:gosec - "crypto/sha1" - "encoding/hex" - "io" - - "github.com/pdok/mapserver-operator/internal/controller/constants" - - corev1 "k8s.io/api/core/v1" -) - -type EnvFromSourceType string - -const ( - EnvFromSourceTypeConfigMap EnvFromSourceType = "configMap" - EnvFromSourceTypeSecret EnvFromSourceType = "secret" -) - -func NewEnvFromSource(t EnvFromSourceType, name string) corev1.EnvFromSource { - switch t { - case EnvFromSourceTypeConfigMap: - return corev1.EnvFromSource{ - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: name, - }, - }, - } - case EnvFromSourceTypeSecret: - return corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: name, - }, - }, - } - default: - return corev1.EnvFromSource{} - } -} - -func GetBaseVolumeMount() corev1.VolumeMount { - return corev1.VolumeMount{Name: constants.BaseVolumeName, MountPath: "/srv/data"} -} - -func GetDataVolumeMount() corev1.VolumeMount { - return corev1.VolumeMount{Name: constants.DataVolumeName, MountPath: "/var/www", ReadOnly: false} -} - -func GetConfigVolumeMount(volumeName string) corev1.VolumeMount { - return corev1.VolumeMount{Name: volumeName, MountPath: "/input", ReadOnly: true} -} - -func GetMapfileVolumeMount() corev1.VolumeMount { - return corev1.VolumeMount{Name: constants.ConfigMapCustomMapfileVolumeName, MountPath: "/srv/data/config/mapfile"} -} - -func Sha1Hash(v string) string { - //nolint:gosec - s := sha1.New() - _, _ = io.WriteString(s, v) - - return hex.EncodeToString(s.Sum(nil)) -} diff --git a/internal/controller/wfs_controller.go b/internal/controller/wfs_controller.go index 7b0aeb4..b2187f9 100644 --- a/internal/controller/wfs_controller.go +++ b/internal/controller/wfs_controller.go @@ -1,25 +1,17 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package controller @@ -27,111 +19,45 @@ package controller import ( "context" - "github.com/pdok/mapserver-operator/internal/controller/types" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorstatus "github.com/pdok/smooth-operator/pkg/status" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) // WFSReconciler reconciles a WFS object type WFSReconciler struct { client.Client Scheme *runtime.Scheme - Images types.Images } // +kubebuilder:rbac:groups=pdok.nl,resources=wfs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=pdok.nl,resources=wfs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=pdok.nl,resources=wfs/finalizers,verbs=update -// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo,verbs=get;list;watch -// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo/status,verbs=get -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete -// +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch -// +kubebuilder:rbac:groups=core,resources=configmaps;services,verbs=watch;create;get;update;list;delete -// +kubebuilder:rbac:groups=core,resources=secrets,verbs=watch;list;get -// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=watch;create;get;update;list;delete -// +kubebuilder:rbac:groups=traefik.io,resources=ingressroutes;middlewares,verbs=get;list;watch;create;update;delete -// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=create;update;delete;list;watch -// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/status,verbs=get;update -// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// The Reconcile function compares the state specified by +// TODO(user): Modify the Reconcile function to compare the state specified by // the WFS object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile -func (r *WFSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { - lgr := log.FromContext(ctx) - lgr.Info("Starting reconcile for WFS resource", "name", req.NamespacedName) - - // Fetch the WFS instance - wfs := &pdoknlv3.WFS{} - if err = r.Client.Get(ctx, req.NamespacedName, wfs); err != nil { - if apierrors.IsNotFound(err) { - lgr.Info("WFS resource not found", "name", req.NamespacedName) - } else { - lgr.Error(err, "unable to fetch WFS resource", "error", err) - } - return result, client.IgnoreNotFound(err) - } - - lgr.Info("Fetching OwnerInfo", "name", req.NamespacedName) - // Fetch the OwnerInfo instance - ownerInfo := &smoothoperatorv1.OwnerInfo{} - objectKey := client.ObjectKey{ - Namespace: wfs.Namespace, - Name: wfs.Spec.Service.OwnerInfoRef, - } - if err := r.Client.Get(ctx, objectKey, ownerInfo); err != nil { - if apierrors.IsNotFound(err) { - lgr.Info("OwnerInfo resource not found", "name", req.NamespacedName) - } else { - lgr.Error(err, "unable to fetch OwnerInfo resource", "error", err) - } - return result, err - } - - // Recover from a panic so we can add the error to the status of the Atom - defer func() { - if rec := recover(); rec != nil { - err = recoveredPanicToError(rec) - smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wfs, err) - } - }() - - // Check TTL, delete if expired - if ttlExpired(wfs) { - err = r.Client.Delete(ctx, wfs) - - return result, err - } - - ensureLabel(wfs, "pdok.nl/service-type", "wfs") +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile +func (r *WFSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) - lgr.Info("creating resources for wfs", "wfs", wfs.Name) - operationResults, err := createOrUpdateAllForWMSWFS(ctx, r, wfs, ownerInfo) - if err != nil { - lgr.Info("failed creating resources for wfs", "wfs", wfs.Name) - smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wfs, err) - return result, err - } - lgr.Info("finished creating resources for wfs", "wfs", wfs.Name) - smoothoperatorstatus.LogAndUpdateStatusFinished(ctx, r.Client, wfs, operationResults) + // TODO(user): your logic here - return result, err + return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *WFSReconciler) SetupWithManager(mgr ctrl.Manager) error { - return createControllerManager(mgr, &pdoknlv3.WFS{}).Complete(r) + return ctrl.NewControllerManagedBy(mgr). + For(&pdoknlv3.WFS{}). + Named("wfs"). + Complete(r) } diff --git a/internal/controller/wfs_controller_test.go b/internal/controller/wfs_controller_test.go index f542db5..a8796bc 100644 --- a/internal/controller/wfs_controller_test.go +++ b/internal/controller/wfs_controller_test.go @@ -1,322 +1,84 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package controller import ( "context" - "fmt" - "os" - - "github.com/pdok/mapserver-operator/internal/controller/types" - "github.com/pdok/smooth-operator/model" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - k8stypes "k8s.io/apimachinery/pkg/types" - "k8s.io/utils/ptr" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd - . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/yaml" -) - -var _ = Describe("Testing WFS Controller", func() { - - Context("Testing Mutate functions for Minimal WFS", func() { - testMutates(getWFSReconciler, &pdoknlv3.WFS{}, "minimal") - }) - Context("Testing Mutate functions for Complete WFS", func() { - testMutates(getWFSReconciler, &pdoknlv3.WFS{}, "complete") - }) + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - Context("Testing Mutate functions for WFS with prefetchData false", func() { - testMutates(getWFSReconciler, &pdoknlv3.WFS{}, "noprefetch", "configmap-init-scripts.yaml") - }) + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" +) +var _ = Describe("WFS Controller", func() { Context("When reconciling a resource", func() { + const resourceName = "test-resource" ctx := context.Background() - inputPath := testPath(pdoknlv3.ServiceTypeWFS, "complete") + "input/" - - testWfs := pdoknlv3.WFS{} - clusterWfs := &pdoknlv3.WFS{} - - objectKeyWfs := k8stypes.NamespacedName{} - - testOwner := smoothoperatorv1.OwnerInfo{} - clusterOwner := &smoothoperatorv1.OwnerInfo{} - - objectKeyOwner := k8stypes.NamespacedName{} - - var expectedResources []struct { - obj client.Object - key k8stypes.NamespacedName + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed } - - It("Should create a WFS and OwnerInfo resource on the cluster", func() { - - By("Creating a new resource for the Kind WFS") - data, err := readTestFile(inputPath + "wfs.yaml") - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, &testWfs) - Expect(err).NotTo(HaveOccurred()) - Expect(testWfs.Name).Should(Equal("complete")) - - objectKeyWfs = k8stypes.NamespacedName{ - Namespace: testWfs.GetNamespace(), - Name: testWfs.GetName(), - } - - err = k8sClient.Get(ctx, objectKeyWfs, clusterWfs) - Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) - if err != nil && apierrors.IsNotFound(err) { - resource := testWfs.DeepCopy() + wfs := &pdoknlv3.WFS{} + + BeforeEach(func() { + By("creating the custom resource for the Kind WFS") + err := k8sClient.Get(ctx, typeNamespacedName, wfs) + if err != nil && errors.IsNotFound(err) { + resource := &pdoknlv3.WFS{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) } - - By("Creating a new resource for the Kind OwnerInfo") - data, err = os.ReadFile(inputPath + "ownerinfo.yaml") - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, &testOwner) - Expect(err).NotTo(HaveOccurred()) - Expect(testOwner.Name).Should(Equal("owner")) - - objectKeyOwner = k8stypes.NamespacedName{ - Namespace: testOwner.GetNamespace(), - Name: testOwner.GetName(), - } - - err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) - Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) - if err != nil && apierrors.IsNotFound(err) { - resource := testOwner.DeepCopy() - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - Expect(k8sClient.Get(ctx, objectKeyOwner, clusterOwner)).To(Succeed()) - } - }) - - It("Should reconcile successfully", func() { - controllerReconciler := getWFSReconciler() - - By("Reconciling the WFS") - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) - Expect(err).NotTo(HaveOccurred()) }) - It("Should create all expected resources", func() { - expectedResources, err := getExpectedObjects(ctx, clusterWfs, true, true) + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &pdoknlv3.WFS{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - for _, expectedResource := range expectedResources { - Eventually(func() bool { - err := k8sClient.Get(ctx, k8stypes.NamespacedName{Namespace: expectedResource.GetNamespace(), Name: expectedResource.GetName()}, expectedResource) - return Expect(err).NotTo(HaveOccurred()) - }, "10s", "1s").Should(BeTrue()) - } - }) - - It("Should successfully reconcile after a change in an owned resource", func() { - controllerReconciler := getWFSReconciler() - - By("Getting the original Deployment") - deployment := getBareDeployment(clusterWfs) - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) - return Expect(err).NotTo(HaveOccurred()) - }, "10s", "1s").Should(BeTrue()) - originalRevisionHistoryLimit := *deployment.Spec.RevisionHistoryLimit - expectedRevisionHistoryLimit := 99 - Expect(originalRevisionHistoryLimit).Should(Not(Equal(expectedRevisionHistoryLimit))) - - By("Altering the Deployment") - err := k8sClient.Patch(ctx, deployment, client.RawPatch(k8stypes.MergePatchType, []byte( - fmt.Sprintf(`{"spec": {"revisionHistoryLimit": %d}}`, expectedRevisionHistoryLimit)))) - Expect(err).NotTo(HaveOccurred()) - - By("Verifying that the Deployment was altered") - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) - return Expect(err).NotTo(HaveOccurred()) && - Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(expectedRevisionHistoryLimit)) - }, "10s", "1s").Should(BeTrue()) - - By("Reconciling the WFS again") - _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) - Expect(err).NotTo(HaveOccurred()) - - By("Verifying that the Deployment was restored") - Eventually(func() bool { - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) - return Expect(err).NotTo(HaveOccurred()) && - Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(originalRevisionHistoryLimit)) - }, "10s", "1s").Should(BeTrue()) - }) - - It("Should delete PodDisruptionBudget if Min and Max replicas == 1 ", func() { - controllerReconciler := getWFSReconciler() - - By("Setting Min and Max replicas to 1") - - Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) - - resource := clusterWfs.DeepCopy() - - resource.Spec.HorizontalPodAutoscalerPatch.MinReplicas = ptr.To(int32(1)) - resource.Spec.HorizontalPodAutoscalerPatch.MaxReplicas = ptr.To(int32(1)) - - Expect(k8sClient.Update(ctx, resource)).To(Succeed()) - - podDisruptionBudget := getBarePodDisruptionBudget(resource) - - By("Reconciling the WFS") - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) - Expect(err).NotTo(HaveOccurred()) - - By("Getting the PodDisruptionBudget") - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(podDisruptionBudget), podDisruptionBudget) - Expect(apierrors.IsNotFound(err)).To(BeTrue()) - - Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) - Expect(clusterWfs.Status.OperationResults[smoothoperatorutils.GetObjectFullName(k8sClient, podDisruptionBudget)]).To(Equal(controllerutil.OperationResult("deleted"))) - }) - - It("Should not Create PodDisruptionBudget if Min and Max replicas == 1 ", func() { - controllerReconciler := getWFSReconciler() - - By("Getting Cluster WFS Min and Max replicas to 1") - Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) - - Expect(clusterWfs.HorizontalPodAutoscalerPatch().MaxReplicas).To(Equal(ptr.To(int32(1)))) - Expect(clusterWfs.HorizontalPodAutoscalerPatch().MinReplicas).To(Equal(ptr.To(int32(1)))) - - podDisruptionBudget := getBarePodDisruptionBudget(clusterWfs) - - By("Reconciling the WFS") - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) - Expect(err).NotTo(HaveOccurred()) - - By("Getting the PodDisruptionBudget") - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(podDisruptionBudget), podDisruptionBudget) - Expect(apierrors.IsNotFound(err)).To(BeTrue()) - - Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) - _, ok := clusterWfs.Status.OperationResults[smoothoperatorutils.GetObjectFullName(k8sClient, podDisruptionBudget)] - Expect(ok).To(BeFalse()) - }) - - It("Respects the TTL of the WFS", func() { - By("Creating a new resource for the Kind WFS") - - ttlName := testWfs.GetName() + "-ttl" - ttlWfs := testWfs.DeepCopy() - ttlWfs.Name = ttlName - ttlWfs.Spec.Lifecycle = &model.Lifecycle{TTLInDays: smoothoperatorutils.Pointer(int32(0))} - objectKeyTTLWFS := client.ObjectKeyFromObject(ttlWfs) - - err := k8sClient.Get(ctx, objectKeyTTLWFS, ttlWfs) - Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) - if err != nil && apierrors.IsNotFound(err) { - Expect(k8sClient.Create(ctx, ttlWfs)).To(Succeed()) - } - - // Reconcile - _, err = getWFSReconciler().Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyTTLWFS}) - Expect(err).To(Not(HaveOccurred())) - - // Check the WFS cannot be found anymore - Eventually(func() bool { - err = k8sClient.Get(ctx, objectKeyTTLWFS, ttlWfs) - return apierrors.IsNotFound(err) - }, "10s", "1s").Should(BeTrue()) - - // Not checking owned resources because the test env does not do garbage collection - }) - - It("Should cleanup the cluster", func() { - err := k8sClient.Get(ctx, objectKeyWfs, clusterWfs) - Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred()) By("Cleanup the specific resource instance WFS") - Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, clusterWfs))).To(Succeed()) - - err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) - Expect(err).NotTo(HaveOccurred()) - - By("Cleanup the specific resource instance OwnerInfo") - Expect(k8sClient.Delete(ctx, clusterOwner)).To(Succeed()) - - // the testEnv does not do garbage collection (https://book.kubebuilder.io/reference/envtest#testing-considerations) - By("Cleaning Owned Resources") - for _, d := range expectedResources { - err := k8sClient.Get(ctx, d.key, d.obj) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, d.obj)).To(Succeed()) - } + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) }) - }) - - Context("When manually validating an incoming CRD", func() { - It("Should not error", func() { - err := smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wfs.pdok.nl") - Expect(err).NotTo(HaveOccurred()) - - filepath := "input/wfs.yaml" - testCases := []string{ - testPath(pdoknlv3.ServiceTypeWFS, "minimal") + filepath, - testPath(pdoknlv3.ServiceTypeWFS, "complete") + filepath, - testPath(pdoknlv3.ServiceTypeWFS, "noprefetch") + filepath, + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &WFSReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), } - for _, test := range testCases { - yamlInput, err := readTestFile(test) - Expect(err).NotTo(HaveOccurred()) - - err = smoothoperatorvalidation.ValidateSchema(string(yamlInput)) - Expect(err).NotTo(HaveOccurred()) - } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. }) }) }) - -func getWFSReconciler() *WFSReconciler { - return &WFSReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - Images: types.Images{ - MultitoolImage: testImageName1, - MapfileGeneratorImage: testImageName2, - MapserverImage: testImageName3, - CapabilitiesGeneratorImage: testImageName4, - ApacheExporterImage: testImageName5, - }, - } -} diff --git a/internal/controller/wms_controller.go b/internal/controller/wms_controller.go index e486233..0558be9 100644 --- a/internal/controller/wms_controller.go +++ b/internal/controller/wms_controller.go @@ -1,25 +1,17 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package controller @@ -27,16 +19,6 @@ package controller import ( "context" - "github.com/pdok/mapserver-operator/internal/controller/types" - - "github.com/pdok/mapserver-operator/internal/controller/featureinfogenerator" - "github.com/pdok/mapserver-operator/internal/controller/legendgenerator" - "github.com/pdok/mapserver-operator/internal/controller/ogcwebserviceproxy" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorstatus "github.com/pdok/smooth-operator/pkg/status" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,174 +27,37 @@ import ( pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) -const ( - ogcWebserviceProxyInput = "service-config.yaml" - featureinfoGeneratorInput = "input.json" -) - // WMSReconciler reconciles a WMS object type WMSReconciler struct { client.Client Scheme *runtime.Scheme - Images types.Images } // +kubebuilder:rbac:groups=pdok.nl,resources=wms,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=pdok.nl,resources=wms/status,verbs=get;update;patch // +kubebuilder:rbac:groups=pdok.nl,resources=wms/finalizers,verbs=update -// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo,verbs=get;list;watch -// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo/status,verbs= -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete -// +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch -// +kubebuilder:rbac:groups=core,resources=configmaps;services,verbs=watch;create;get;update;list;delete -// +kubebuilder:rbac:groups=core,resources=secrets,verbs=watch;list;get -// +kubebuilder:rbac:groups=traefik.io,resources=ingressroutes;middlewares,verbs=get;list;watch;create;update;delete -// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=watch;create;get;update;list;delete -// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=create;update;delete;list;watch -// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/status,verbs=get;update -// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// The Reconcile function compares the state specified by +// TODO(user): Modify the Reconcile function to compare the state specified by // the WMS object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile -func (r *WMSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { - lgr := log.FromContext(ctx) - lgr.Info("Starting reconcile for WMS resource", "name", req.NamespacedName) - - // Fetch the WMS instance - wms := &pdoknlv3.WMS{} - if err = r.Client.Get(ctx, req.NamespacedName, wms); err != nil { - if apierrors.IsNotFound(err) { - lgr.Info("WMS resource not found", "name", req.NamespacedName) - } else { - lgr.Error(err, "unable to fetch WMS resource", "error", err) - } - return result, client.IgnoreNotFound(err) - } - - lgr.Info("Fetching OwnerInfo", "name", req.NamespacedName) - // Fetch the OwnerInfo instance - ownerInfo := &smoothoperatorv1.OwnerInfo{} - objectKey := client.ObjectKey{ - Namespace: wms.Namespace, - Name: wms.Spec.Service.OwnerInfoRef, - } - if err := r.Client.Get(ctx, objectKey, ownerInfo); err != nil { - if apierrors.IsNotFound(err) { - lgr.Info("OwnerInfo resource not found", "name", req.NamespacedName) - } else { - lgr.Error(err, "unable to fetch OwnerInfo resource", "error", err) - } - return result, client.IgnoreNotFound(err) - } - - // Recover from a panic so we can add the error to the status of the Atom - defer func() { - if rec := recover(); rec != nil { - err = recoveredPanicToError(rec) - smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wms, err) - } - }() - - // Check TTL, delete if expired - if ttlExpired(wms) { - err = r.Client.Delete(ctx, wms) - - return result, err - } - - ensureLabel(wms, "pdok.nl/service-type", "wms") - - lgr.Info("creating resources for wms", "wms", wms.Name) - operationResults, err := createOrUpdateAllForWMSWFS(ctx, r, wms, ownerInfo) - if err != nil { - lgr.Info("failed creating resources for wms", "wms", wms.Name) - smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wms, err) - return result, err - } - lgr.Info("finished creating resources for wms", "wms", wms.Name) - smoothoperatorstatus.LogAndUpdateStatusFinished(ctx, r.Client, wms, operationResults) - - return result, err -} - -func mutateConfigMapLegendGenerator(r *WMSReconciler, wms *pdoknlv3.WMS, configMap *corev1.ConfigMap) error { - labels := addCommonLabels(wms, smoothoperatorutils.CloneOrEmptyMap(wms.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(r.Client, configMap, labels); err != nil { - return err - } - - if len(configMap.Data) == 0 { - configMap.Data = legendgenerator.GetConfigMapData(wms) - } - configMap.Immutable = smoothoperatorutils.Pointer(true) - - if err := smoothoperatorutils.EnsureSetGVK(r.Client, configMap, configMap); err != nil { - return err - } - if err := ctrl.SetControllerReference(wms, configMap, getReconcilerScheme(r)); err != nil { - return err - } - return smoothoperatorutils.AddHashSuffix(configMap) - -} - -func mutateConfigMapFeatureinfoGenerator(r *WMSReconciler, wms *pdoknlv3.WMS, configMap *corev1.ConfigMap) error { - labels := addCommonLabels(wms, smoothoperatorutils.CloneOrEmptyMap(wms.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(r.Client, configMap, labels); err != nil { - return err - } - - if len(configMap.Data) == 0 { - input, err := featureinfogenerator.GetInput(wms) - if err != nil { - return err - } - configMap.Data = map[string]string{featureinfoGeneratorInput: input} - } - configMap.Immutable = smoothoperatorutils.Pointer(true) - - if err := smoothoperatorutils.EnsureSetGVK(r.Client, configMap, configMap); err != nil { - return err - } - if err := ctrl.SetControllerReference(wms, configMap, r.Scheme); err != nil { - return err - } - return smoothoperatorutils.AddHashSuffix(configMap) -} - -func mutateConfigMapOgcWebserviceProxy(r *WMSReconciler, wms *pdoknlv3.WMS, configMap *corev1.ConfigMap) error { - - labels := addCommonLabels(wms, smoothoperatorutils.CloneOrEmptyMap(wms.GetLabels())) - if err := smoothoperatorutils.SetImmutableLabels(r.Client, configMap, labels); err != nil { - return err - } +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile +func (r *WMSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) - if len(configMap.Data) == 0 { - input, err := ogcwebserviceproxy.GetConfig(wms) - if err != nil { - return err - } - configMap.Data = map[string]string{ogcWebserviceProxyInput: input} - } - configMap.Immutable = smoothoperatorutils.Pointer(true) + // TODO(user): your logic here - if err := smoothoperatorutils.EnsureSetGVK(r.Client, configMap, configMap); err != nil { - return err - } - if err := ctrl.SetControllerReference(wms, configMap, getReconcilerScheme(r)); err != nil { - return err - } - return smoothoperatorutils.AddHashSuffix(configMap) + return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *WMSReconciler) SetupWithManager(mgr ctrl.Manager) error { - return createControllerManager(mgr, &pdoknlv3.WMS{}).Complete(r) + return ctrl.NewControllerManagedBy(mgr). + For(&pdoknlv3.WMS{}). + Named("wms"). + Complete(r) } diff --git a/internal/controller/wms_controller_test.go b/internal/controller/wms_controller_test.go index 5289ddd..6e25e8b 100644 --- a/internal/controller/wms_controller_test.go +++ b/internal/controller/wms_controller_test.go @@ -1,280 +1,84 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package controller import ( "context" - "fmt" - "os" - - "github.com/pdok/mapserver-operator/internal/controller/types" - "github.com/pdok/smooth-operator/model" - smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" - . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd - . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" - apierrors "k8s.io/apimachinery/pkg/api/errors" - k8stypes "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/yaml" -) - -var _ = Describe("Testing WMS Controller", func() { - Context("Testing Mutate functions for Minimal WMS", func() { - testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "minimal") - }) - - Context("Testing Mutate functions for Minimal WMS without prefetch", func() { - testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "noprefetch", "configmap-init-scripts.yaml") - }) - - Context("Testing Mutate functions for Minimal WMS with a custom mapfile", func() { - testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "custom-mapfile", "configmap-mapfile-generator.yaml") - }) + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - Context("Testing Mutate functions for Complete WMS", func() { - testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "complete") - }) - - Context("Testing Mutate functions for WMS with extreme patches", func() { - testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "patches") - }) + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" +) +var _ = Describe("WMS Controller", func() { Context("When reconciling a resource", func() { + const resourceName = "test-resource" ctx := context.Background() - inputPath := testPath(pdoknlv3.ServiceTypeWMS, "complete") + "input/" - - testWMS := pdoknlv3.WMS{} - clusterWMS := &pdoknlv3.WMS{} - - objectKeyWMS := k8stypes.NamespacedName{} - - testOwner := smoothoperatorv1.OwnerInfo{} - clusterOwner := &smoothoperatorv1.OwnerInfo{} - - objectKeyOwner := k8stypes.NamespacedName{} - - var expectedResources []struct { - obj client.Object - key k8stypes.NamespacedName + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", // TODO(user):Modify as needed } - - It("Should create a WMS and OwnerInfo resource on the cluster", func() { - - By("Creating a new resource for the Kind WMS") - data, err := readTestFile(inputPath + "wms.yaml") - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, &testWMS) - Expect(err).NotTo(HaveOccurred()) - Expect(testWMS.Name).Should(Equal("complete")) - - objectKeyWMS = k8stypes.NamespacedName{ - Namespace: testWMS.GetNamespace(), - Name: testWMS.GetName(), - } - - err = k8sClient.Get(ctx, objectKeyWMS, clusterWMS) - Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) - if err != nil && apierrors.IsNotFound(err) { - resource := testWMS.DeepCopy() - Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - Expect(k8sClient.Get(ctx, objectKeyWMS, clusterWMS)).To(Succeed()) - } - - By("Creating a new resource for the Kind OwnerInfo") - data, err = os.ReadFile(inputPath + "ownerinfo.yaml") - Expect(err).NotTo(HaveOccurred()) - err = yaml.UnmarshalStrict(data, &testOwner) - Expect(err).NotTo(HaveOccurred()) - Expect(testOwner.Name).Should(Equal("owner")) - - objectKeyOwner = k8stypes.NamespacedName{ - Namespace: testOwner.GetNamespace(), - Name: testOwner.GetName(), - } - - err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) - Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) - if err != nil && apierrors.IsNotFound(err) { - resource := testOwner.DeepCopy() + wms := &pdoknlv3.WMS{} + + BeforeEach(func() { + By("creating the custom resource for the Kind WMS") + err := k8sClient.Get(ctx, typeNamespacedName, wms) + if err != nil && errors.IsNotFound(err) { + resource := &pdoknlv3.WMS{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + // TODO(user): Specify other spec details if needed. + } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) - Expect(k8sClient.Get(ctx, objectKeyOwner, clusterOwner)).To(Succeed()) } }) - It("Should reconcile successfully", func() { - controllerReconciler := getWMSReconciler() - - By("Reconciling the WMS") - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWMS}) + AfterEach(func() { + // TODO(user): Cleanup logic after each test, like removing the resource instance. + resource := &pdoknlv3.WMS{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - }) - - It("Should create all expected resources", func() { - expectedResources, err := getExpectedObjects(ctx, clusterWMS, true, true) - Expect(err).NotTo(HaveOccurred()) - - for _, expectedResource := range expectedResources { - Eventually(func() bool { - err := k8sClient.Get(ctx, k8stypes.NamespacedName{Namespace: expectedResource.GetNamespace(), Name: expectedResource.GetName()}, expectedResource) - return Expect(err).NotTo(HaveOccurred()) - }, "10s", "1s").Should(BeTrue()) - } - }) - - It("Should successfully reconcile after a change in an owned resource", func() { - controllerReconciler := getWMSReconciler() - - By("Getting the original Deployment") - deployment := getBareDeployment(clusterWMS) - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) - return Expect(err).NotTo(HaveOccurred()) - }, "10s", "1s").Should(BeTrue()) - originalRevisionHistoryLimit := *deployment.Spec.RevisionHistoryLimit - expectedRevisionHistoryLimit := 99 - Expect(originalRevisionHistoryLimit).Should(Not(Equal(expectedRevisionHistoryLimit))) - - By("Altering the Deployment") - err := k8sClient.Patch(ctx, deployment, client.RawPatch(k8stypes.MergePatchType, []byte( - fmt.Sprintf(`{"spec": {"revisionHistoryLimit": %d}}`, expectedRevisionHistoryLimit)))) - Expect(err).NotTo(HaveOccurred()) - - By("Verifying that the Deployment was altered") - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) - return Expect(err).NotTo(HaveOccurred()) && - Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(expectedRevisionHistoryLimit)) - }, "10s", "1s").Should(BeTrue()) - - By("Reconciling the WMS again") - _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWMS}) - Expect(err).NotTo(HaveOccurred()) - - By("Verifying that the Deployment was restored") - Eventually(func() bool { - err = k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) - return Expect(err).NotTo(HaveOccurred()) && - Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(originalRevisionHistoryLimit)) - }, "10s", "1s").Should(BeTrue()) - }) - - It("Respects the TTL of the WMS", func() { - By("Creating a new resource for the Kind WMS") - - ttlName := testWMS.GetName() + "-ttl" - ttlWms := testWMS.DeepCopy() - ttlWms.Name = ttlName - ttlWms.Spec.Lifecycle = &model.Lifecycle{TTLInDays: smoothoperatorutils.Pointer(int32(0))} - objectKeyTTLWMS := client.ObjectKeyFromObject(ttlWms) - - err := k8sClient.Get(ctx, objectKeyTTLWMS, ttlWms) - Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) - if err != nil && apierrors.IsNotFound(err) { - Expect(k8sClient.Create(ctx, ttlWms)).To(Succeed()) - } - - // Reconcile - _, err = getWMSReconciler().Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyTTLWMS}) - Expect(err).To(Not(HaveOccurred())) - - // Check the WMS cannot be found anymore - Eventually(func() bool { - err = k8sClient.Get(ctx, objectKeyTTLWMS, ttlWms) - return apierrors.IsNotFound(err) - }, "10s", "1s").Should(BeTrue()) - - // Not checking owned resources because the test env does not do garbage collection - }) - - It("Should cleanup the cluster", func() { - err := k8sClient.Get(ctx, objectKeyWMS, clusterWMS) - Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred()) By("Cleanup the specific resource instance WMS") - Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, clusterWMS))).To(Succeed()) - - err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) - Expect(err).NotTo(HaveOccurred()) - - By("Cleanup the specific resource instance OwnerInfo") - Expect(k8sClient.Delete(ctx, clusterOwner)).To(Succeed()) - - // the testEnv does not do garbage collection (https://book.kubebuilder.io/reference/envtest#testing-considerations) - By("Cleaning Owned Resources") - for _, d := range expectedResources { - err := k8sClient.Get(ctx, d.key, d.obj) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient.Delete(ctx, d.obj)).To(Succeed()) - } + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) }) - }) - - Context("When manually validating an incoming CRD", func() { - It("Should not error", func() { - err := smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wms.pdok.nl") - Expect(err).NotTo(HaveOccurred()) - - filepath := "input/wms.yaml" - testCases := []string{ - testPath(pdoknlv3.ServiceTypeWMS, "minimal") + filepath, - testPath(pdoknlv3.ServiceTypeWMS, "complete") + filepath, - testPath(pdoknlv3.ServiceTypeWMS, "noprefetch") + filepath, - testPath(pdoknlv3.ServiceTypeWMS, "custom-mapfile") + filepath, + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &WMSReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), } - for _, test := range testCases { - yamlInput, err := readTestFile(test) - Expect(err).NotTo(HaveOccurred()) - - err = smoothoperatorvalidation.ValidateSchema(string(yamlInput)) - Expect(err).NotTo(HaveOccurred()) - } + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. }) }) }) - -func getWMSReconciler() *WMSReconciler { - return &WMSReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), - Images: types.Images{ - MultitoolImage: testImageName1, - MapfileGeneratorImage: testImageName2, - MapserverImage: testImageName3, - CapabilitiesGeneratorImage: testImageName4, - FeatureinfoGeneratorImage: testImageName5, - OgcWebserviceProxyImage: testImageName6, - ApacheExporterImage: testImageName7, - }, - } -} diff --git a/internal/webhook/v3/shared_webhook.go b/internal/webhook/v3/shared_webhook.go deleted file mode 100644 index 19db028..0000000 --- a/internal/webhook/v3/shared_webhook.go +++ /dev/null @@ -1,114 +0,0 @@ -package v3 - -import ( - "context" - "encoding/json" - "errors" - "os" - - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - k8stypes "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - "sigs.k8s.io/yaml" -) - -const ( - samplesPath = "test_data/" -) - -func readOwnerInfo(ownerInfo *smoothoperatorv1.OwnerInfo) error { - data, err := os.ReadFile(samplesPath + "ownerinfo.yaml") - if err != nil { - return err - } - err = yaml.UnmarshalStrict(data, ownerInfo) - if err != nil { - return err - } - return err -} - -func createOwnerInfo(ctx context.Context, c client.Client, ownerInfo *smoothoperatorv1.OwnerInfo) error { - clusterOwner := &smoothoperatorv1.OwnerInfo{} - objectKeyOwner := k8stypes.NamespacedName{ - Namespace: ownerInfo.GetNamespace(), - Name: ownerInfo.GetName(), - } - - err := c.Get(ctx, objectKeyOwner, clusterOwner) - if client.IgnoreNotFound(err) != nil { - return err - } - if err != nil && apierrors.IsNotFound(err) { - resource := ownerInfo.DeepCopy() - err = c.Create(ctx, resource) - if err != nil { - return err - } - err = c.Get(ctx, objectKeyOwner, clusterOwner) - if err != nil { - return err - } - } - return nil -} - -func updateOwnerInfo(ctx context.Context, c client.Client, ownerInfo *smoothoperatorv1.OwnerInfo) error { - clusterOwner := &smoothoperatorv1.OwnerInfo{} - objectKeyOwner := k8stypes.NamespacedName{ - Namespace: ownerInfo.GetNamespace(), - Name: ownerInfo.GetName(), - } - - err := c.Get(ctx, objectKeyOwner, clusterOwner) - if err != nil { - return err - } - - ownerInfo.ResourceVersion = clusterOwner.ResourceVersion - - err = c.Update(ctx, ownerInfo) - if err != nil { - return err - } - - return nil -} - -func getSampleFilename[W pdoknlv3.WMSWFS](webservice W) (string, error) { - switch any(webservice).(type) { - case *pdoknlv3.WFS: - if _, ok := any(webservice).(*pdoknlv3.WFS); ok { - return samplesPath + "v3_wfs.yaml", nil - } - case *pdoknlv3.WMS: - if _, ok := any(webservice).(*pdoknlv3.WMS); ok { - return samplesPath + "v3_wms.yaml", nil - } - } - return "", errors.New("unknown webservice type, cannot determine sample filename") -} - -func readSample[W pdoknlv3.WMSWFS](webservice W) error { - sampleFilename, err := getSampleFilename(webservice) - if err != nil { - return err - } - sampleYaml, err := os.ReadFile(sampleFilename) - if err != nil { - return err - } - sampleJSON, err := yaml.YAMLToJSONStrict(sampleYaml) - if err != nil { - return err - } - err = json.Unmarshal(sampleJSON, webservice) - if err != nil { - return err - } - - return nil -} diff --git a/internal/webhook/v3/test_data/ownerinfo.yaml b/internal/webhook/v3/test_data/ownerinfo.yaml deleted file mode 100644 index 76d1eba..0000000 --- a/internal/webhook/v3/test_data/ownerinfo.yaml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: pdok.nl/v1 -kind: OwnerInfo -metadata: - name: owner - namespace: default -spec: - metadataUrls: - csw: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" - type: alternate - openSearch: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" - type: alternate - html: - hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" - type: alternate - namespaceTemplate: "http://{{prefix}}.geonovum.nl" - providerSite: - type: simple - href: https://pdok.nl - wfs: - serviceProvider: - providerName: PDOK - wms: - contactInformation: - contactPersonPrimary: - contactPerson: KlantContactCenter PDOK - contactOrganization: PDOK - contactPosition: pointOfContact - contactAddress: - addressType: - address: - city: Apeldoorn - stateOrProvince: - postCode: - country: Netherlands - contactVoiceTelephone: - contactFacsimileTelephone: - contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/webhook/v3/test_data/v3_wfs.yaml b/internal/webhook/v3/test_data/v3_wfs.yaml deleted file mode 100644 index 10a0d6c..0000000 --- a/internal/webhook/v3/test_data/v3_wfs.yaml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WFS -metadata: - name: sample - namespace: default - labels: - sample: sample -spec: - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 2G - ingressRouteUrls: - - url: "http://host/path" - - url: "http://old/path" - service: - inspire: - language: dut - serviceMetadataUrl: - csw: - metadataIdentifier: 655549bd-8c05-4c69-950b-ad1e346dcac9 - spatialDatasetIdentifier: 90af202c-de3a-4fbf-901c-82ae703904e3 - title: "title" - abstract: "abstract" - defaultCrs: "EPSG:28992" - keywords: - - "keyword" - ownerInfoRef: "owner" - prefix: "prefix" - url: "http://host/path" - bbox: - defaultCRS: - maxx: "1" - maxy: "2" - minx: "3" - miny: "4" - featureTypes: - - name: "name" - title: "title" - abstract: "abstract" - keywords: - - "word" - datasetMetadataUrl: - csw: - metadataIdentifier: 8ec62a28-695f-4f46-a9d5-0aeb8363a0e3 - bbox: - defaultCRS: - maxx: "1" - maxy: "2" - minx: "3" - miny: "4" - data: - gpkg: - blobKey: "container/prefix/file.gpkg" - columns: - - name: column - geometryType: Point - tableName: table - - name: "second_name" - title: "title" - abstract: "abstract" - keywords: - - "word" - datasetMetadataUrl: - csw: - metadataIdentifier: 8ec62a28-695f-4f46-a9d5-0aeb8363a0e3 - data: - postgis: - tableName: table - geometryType: Point - columns: - - name: column diff --git a/internal/webhook/v3/test_data/v3_wms.yaml b/internal/webhook/v3/test_data/v3_wms.yaml deleted file mode 100644 index 6ea323c..0000000 --- a/internal/webhook/v3/test_data/v3_wms.yaml +++ /dev/null @@ -1,75 +0,0 @@ -apiVersion: pdok.nl/v3 -kind: WMS -metadata: - name: sample - namespace: default - labels: - sample: sample -spec: - podSpecPatch: - containers: - - name: mapserver - resources: - limits: - ephemeral-storage: 1G - ingressRouteUrls: - - url: "https://test.test/path" - service: - prefix: "prefix" - url: "https://test.test/path" - title: "title" - abstract: "abstract" - keywords: - - "keyword" - ownerInfoRef: "owner" - dataEPSG: "EPSG:28992" - stylingAssets: - configMapRefs: - - name: configmap - keys: - - file.style - layer: - title: "title" - abstract: "abstract" - keywords: - - "keyword" - visible: true - layers: - - name: "visible" - visible: true - title: "title" - abstract: "abstract" - keywords: - - keyword - data: - gpkg: - blobKey: "container/path/file.gpkg" - columns: - - name: "column" - geometryType: "Point" - tableName: "table" - styles: - - name: "name" - title: "title" - visualization: "file.style" - - name: "visible Group Layer" - visible: true - title: title - abstract: abstract - keywords: - - keyword - styles: - - name: style - title: style - layers: - - name: "not visible" - visible: false - data: - postgis: - columns: - - name: "column" - geometryType: "Point" - tableName: "table" - styles: - - name: "style" - visualization: "file.style" diff --git a/internal/webhook/v3/webhook_suite_test.go b/internal/webhook/v3/webhook_suite_test.go index ea12f53..8486337 100644 --- a/internal/webhook/v3/webhook_suite_test.go +++ b/internal/webhook/v3/webhook_suite_test.go @@ -1,58 +1,35 @@ /* -MIT License - -Copyright (c) 2024 Publieke Dienstverlening op de Kaart - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v3 -//nolint:revive // Complains about the dot imports import ( "context" "crypto/tls" - "encoding/json" - "errors" "fmt" "net" "os" - "os/exec" "path/filepath" "testing" "time" - "github.com/pdok/smooth-operator/pkg/validation" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/validation/field" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - "golang.org/x/tools/go/packages" - - pdoknlv2beta1 "github.com/pdok/mapserver-operator/api/v2beta1" - "k8s.io/apimachinery/pkg/runtime" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -63,7 +40,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - admissionv1 "k8s.io/api/admission/v1" // +kubebuilder:scaffold:imports ) @@ -87,39 +63,18 @@ func TestAPIs(t *testing.T) { var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - //nolint:fatcontext ctx, cancel = context.WithCancel(context.TODO()) - scheme := runtime.NewScheme() var err error - err = pdoknlv2beta1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = pdoknlv3.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = admissionv1.AddToScheme(scheme) - Expect(err).NotTo(HaveOccurred()) - - err = smoothoperatorv1.AddToScheme(scheme) + err = pdoknlv3.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") - ownerInfoCRDPath := must(getOwnerInfoCRDPath()) testEnv = &envtest.Environment{ - Scheme: scheme, - CRDDirectoryPaths: []string{ - filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wfs.yaml"), - filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wms.yaml"), - ownerInfoCRDPath, - }, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: false, - CRDInstallOptions: envtest.CRDInstallOptions{ - Scheme: scheme, - // MaxTime: time.Minute, - }, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, @@ -136,14 +91,14 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) // start webhook server using Manager. webhookInstallOptions := &testEnv.WebhookInstallOptions mgr, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme, + Scheme: scheme.Scheme, WebhookServer: webhook.NewServer(webhook.Options{ Host: webhookInstallOptions.LocalServingHost, Port: webhookInstallOptions.LocalServingPort, @@ -207,44 +162,3 @@ func getFirstFoundEnvTestBinaryDir() string { } return "" } - -func getOwnerInfoCRDPath() (string, error) { - smoothOperatorModule, err := getModule("github.com/pdok/smooth-operator") - if err != nil { - return "", err - } - if smoothOperatorModule.Dir == "" { - return "", errors.New("cannot find path for smooth-operator module") - } - return filepath.Join(smoothOperatorModule.Dir, "config", "crd", "bases", "pdok.nl_ownerinfo.yaml"), nil -} - -func getModule(name string) (module *packages.Module, err error) { - out, err := exec.Command("go", "list", "-json", "-m", name).Output() - if err != nil { - return - } - module = &packages.Module{} - err = json.Unmarshal(out, module) - return -} - -func must[T any](t T, err error) T { - if err != nil { - panic(err) - } - return t -} - -func getValidationError[O pdoknlv3.WMSWFS](obj O, errorList *field.Error) error { - return apierrors.NewInvalid(obj.GroupKind(), obj.GetName(), field.ErrorList{errorList}) -} - -func getValidationWarnings[O pdoknlv3.WMSWFS](obj O, path field.Path, warning string, warnings []string) admission.Warnings { - validation.AddWarning(&warnings, path, warning, schema.GroupVersionKind{ - Group: obj.GroupKind().Group, - Version: "v3", - Kind: obj.GroupKind().Kind, - }, obj.GetName()) - return warnings -} diff --git a/internal/webhook/v3/wfs_webhook.go b/internal/webhook/v3/wfs_webhook.go index cae035e..ccbfca4 100644 --- a/internal/webhook/v3/wfs_webhook.go +++ b/internal/webhook/v3/wfs_webhook.go @@ -1,36 +1,25 @@ /* -MIT License - -Copyright (c) 2024 Publieke Dienstverlening op de Kaart - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -//nolint:dupl package v3 import ( "context" "fmt" - "sigs.k8s.io/controller-runtime/pkg/client" - "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -40,16 +29,19 @@ import ( pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) +// nolint:unused // log is for logging in this package. -var wfsLog = logf.Log.WithName("wfs-resource") +var wfslog = logf.Log.WithName("wfs-resource") // SetupWFSWebhookWithManager registers the webhook for WFS in the manager. func SetupWFSWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr).For(&pdoknlv3.WFS{}). - WithValidator(&WFSCustomValidator{mgr.GetClient()}). + WithValidator(&WFSCustomValidator{}). Complete() } +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. // NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. @@ -61,44 +53,44 @@ func SetupWFSWebhookWithManager(mgr ctrl.Manager) error { // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as this struct is used only for temporary operations and does not need to be deeply copied. type WFSCustomValidator struct { - Client client.Client + // TODO(user): Add more fields as needed for validation } var _ webhook.CustomValidator = &WFSCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { wfs, ok := obj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object but got %T", obj) } - wfsLog.Info("Validation for WFS upon creation", "name", wfs.GetName()) + wfslog.Info("Validation for WFS upon creation", "name", wfs.GetName()) + + // TODO(user): fill in your validation logic upon object creation. - return wfs.ValidateCreate(v.Client) + return nil, nil } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { wfs, ok := newObj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object for the newObj but got %T", newObj) } - wfsOld, ok := oldObj.(*pdoknlv3.WFS) - if !ok { - return nil, fmt.Errorf("expected a WFS object for the oldObj but got %T", newObj) - } - wfsLog.Info("Validation for WFS upon update", "name", wfs.GetName()) + wfslog.Info("Validation for WFS upon update", "name", wfs.GetName()) - return wfs.ValidateUpdate(v.Client, wfsOld) + // TODO(user): fill in your validation logic upon object update. + + return nil, nil } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { wfs, ok := obj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object but got %T", obj) } - wfsLog.Info("Validation for WFS upon deletion", "name", wfs.GetName()) + wfslog.Info("Validation for WFS upon deletion", "name", wfs.GetName()) // TODO(user): fill in your validation logic upon object deletion. diff --git a/internal/webhook/v3/wfs_webhook_test.go b/internal/webhook/v3/wfs_webhook_test.go index c406c9b..93abd49 100644 --- a/internal/webhook/v3/wfs_webhook_test.go +++ b/internal/webhook/v3/wfs_webhook_test.go @@ -1,45 +1,27 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v3 -//nolint:revive // Complains about the dot imports import ( - "context" - "fmt" - - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/utils/ptr" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" - smoothoperatormodel "github.com/pdok/smooth-operator/model" - corev1 "k8s.io/api/core/v1" + // TODO (user): Add any additional imports if needed ) var _ = Describe("WFS Webhook", func() { @@ -47,421 +29,53 @@ var _ = Describe("WFS Webhook", func() { obj *pdoknlv3.WFS oldObj *pdoknlv3.WFS validator WFSCustomValidator - ownerInfo *smoothoperatorv1.OwnerInfo ) BeforeEach(func() { - validator = WFSCustomValidator{k8sClient} + obj = &pdoknlv3.WFS{} + oldObj = &pdoknlv3.WFS{} + validator = WFSCustomValidator{} Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") - - sample := &pdoknlv3.WFS{} - err := readSample(sample) - Expect(err).To(BeNil(), "Reading and parsing the WFS V3 sample failed") - - obj = sample.DeepCopy() - oldObj = sample.DeepCopy() - - Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") - - ownerInfoSample := &smoothoperatorv1.OwnerInfo{} - Expect(readOwnerInfo(ownerInfoSample)).To(Succeed(), "Reading and parsing the Ownerinfo sample failed") - ownerInfo = ownerInfoSample.DeepCopy() - Expect(ownerInfo).NotTo(BeNil()) - Expect(createOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests }) AfterEach(func() { - Expect(k8sClient.Delete(ctx, ownerInfo)).To(Succeed()) + // TODO (user): Add any teardown logic common to all tests }) Context("When creating or updating WFS under Validating Webhook", func() { - ctx := context.Background() - - It("Creates the WFS from the sample", func() { - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny creation if there are no labels", func() { - obj.Labels = nil - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("metadata").Child("labels"), - "can't be empty", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when URL not in IngressRouteURLs", func() { - url, err := smoothoperatormodel.ParseURL("http://changed/changed") - Expect(err).To(BeNil()) - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} - url, err = smoothoperatormodel.ParseURL("http://sample/sample") - Expect(err).To(BeNil()) - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("must contain baseURL: %s", url), - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Warns if the name contains WFS", func() { - obj.Name += "-wfs" - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("metadata").Child("name"), - "name should not contain wfs", - []string{}, - ))) - }) - - It("Should deny creation if there is no bounding box and the defaultCRS is not EPSG:28992", func() { - obj.Spec.Service.DefaultCrs = "EPSG:1234" - obj.Spec.Service.Bbox = nil - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("bbox").Child("defaultCRS"), - "when service.defaultCRS is not 'EPSG:28992'", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Warns if the mapfile and service/featuretype bbox are both set", func() { - Expect(obj.Spec.Service.FeatureTypes[0].Bbox).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].Bbox.DefaultCRS).NotTo(BeNil()) - Expect(obj.Spec.Service.Bbox).NotTo(BeNil()) - obj.Spec.Service.Mapfile = &pdoknlv3.Mapfile{} - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("featureTypes").Index(0).Child("bbox").Child("defaultCrs"), - "is not used when service.mapfile is configured", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("bbox"), - "is not used when service.mapfile is configured", - []string{}, - )))) - }) - - It("Should deny Create when a otherCrs has the same crs multiple times", func() { - crs := "EPSG:3035" - obj.Spec.Service.OtherCrs = []string{crs, crs} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Duplicate( - field.NewPath("spec").Child("service").Child("otherCrs").Index(1), - crs, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should warn on creation if SpatialID is also used as a featureType datasetMetadataID", func() { - Expect(obj.Inspire()).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) - obj.Spec.Service.Inspire.SpatialDatasetIdentifier = obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW.MetadataIdentifier - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(admission.Warnings{field.Invalid( - field.NewPath("spec").Child("service").Child("inspire").Child("spatialDatasetIdentifier"), - obj.Spec.Service.Inspire.SpatialDatasetIdentifier, - "spatialDatasetIdentifier should not also be used as an datasetMetadataUrl.csw.metadataIdentifier", - ).Error()})) - }) - - It("Should deny creation if serviceMetadataID is also used as a featureType datasetMetadataID", func() { - Expect(obj.Inspire()).NotTo(BeNil()) - Expect(obj.Inspire().ServiceMetadataURL.CSW).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) - obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier = obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW.MetadataIdentifier - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), - obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier, - "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as an datasetMetadataUrl.csw.metadataIdentifier", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny creation if serviceMetadataID is the same as the SpatialID", func() { - Expect(obj.Inspire()).NotTo(BeNil()) - Expect(obj.Inspire().ServiceMetadataURL.CSW).NotTo(BeNil()) - obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier = obj.Spec.Service.Inspire.SpatialDatasetIdentifier - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), - obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier, - "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as the spatialDatasetIdentifier", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny creation if service is Inspire and not all featureTypes have the same datasetMetadataID", func() { - Expect(obj.Inspire()).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) - Expect(len(obj.Spec.Service.FeatureTypes)).To(BeNumerically(">", 1)) - Expect(obj.Spec.Service.FeatureTypes[1].DatasetMetadataURL).NotTo(BeNil()) - Expect(obj.Spec.Service.FeatureTypes[1].DatasetMetadataURL.CSW).NotTo(BeNil()) - obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW.MetadataIdentifier = "" - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("featureTypes[*]").Child("datasetMetadataUrl").Child("csw").Child("metadataIdentifier"), - obj.DatasetMetadataIDs(), - "when Inspire, all featureTypes need use the same datasetMetadataUrl.csw.metadataIdentifier", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when minReplicas are larger than maxReplicas", func() { - obj.Spec.HorizontalPodAutoscalerPatch = &pdoknlv3.HorizontalPodAutoscalerPatch{ - MinReplicas: ptr.To(int32(10)), - MaxReplicas: ptr.To(int32(5)), - } - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("horizontalPodAutoscaler"), - fmt.Sprintf("minReplicas: %d, maxReplicas: %d", 10, 5), - "maxReplicas cannot be less than minReplicas", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when mapserver container doesn't have ephemeral storage", func() { - obj.Spec.PodSpecPatch = corev1.PodSpec{} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required(field.NewPath("spec"). - Child("podSpecPatch"). - Child("containers"). - Key("mapserver"). - Child("resources"). - Child("limits"). - Child(corev1.ResourceEphemeralStorage.String()), "")))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny creation if multiple featureTypes have the same name", func() { - Expect(len(obj.Spec.Service.FeatureTypes)).To(BeNumerically(">", 1)) - obj.Spec.Service.FeatureTypes[1].Name = obj.Spec.Service.FeatureTypes[0].Name - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Duplicate( - field.NewPath("spec").Child("service").Child("featureTypes").Index(1).Child("name"), - obj.Spec.Service.FeatureTypes[1].Name, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef doesn't exist", func() { - obj.Spec.Service.OwnerInfoRef = "changed" - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.NotFound( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - obj.Spec.Service.OwnerInfoRef, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef misses namespaceTemplate", func() { - ownerInfo.Spec.NamespaceTemplate = nil - - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.namespaceTemplate missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef misses csw metadataTemplate", func() { - obj.Spec.Service.Inspire = &pdoknlv3.WFSInspire{Inspire: pdoknlv3.Inspire{ServiceMetadataURL: pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}}}} - ownerInfo.Spec.MetadataUrls.CSW = nil - - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.metadataUrls.csw missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - - ownerInfo.Spec.MetadataUrls = nil - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err = validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.metadataUrls.csw missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef misses WMS", func() { - ownerInfo.Spec.WFS = nil - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.WFS missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a ingressRouteURL was removed", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).To(BeNil()) - oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ - {URL: obj.URL()}, - {URL: smoothoperatormodel.URL{URL: url}}, - } - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: obj.URL()}} - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("urls cannot be removed, missing: %s", smoothoperatormodel.IngressRouteURL{URL: smoothoperatormodel.URL{URL: url}}), - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should accept update if a url was changed when it's in ingressRouteUrls", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).To(BeNil()) - oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ - {URL: obj.URL()}, - {URL: smoothoperatormodel.URL{URL: url}}, - } - obj.Spec.IngressRouteURLs = oldObj.Spec.IngressRouteURLs - oldObj.Spec.Service.URL = obj.URL() - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a url was changed and ingressRouteUrls = nil", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).To(BeNil()) - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - obj.Spec.IngressRouteURLs = nil - oldObj.Spec.IngressRouteURLs = nil - - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("spec").Child("service").Child("url"), - "is immutable, add the old and new urls to spec.ingressRouteUrls in order to change this field", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update url was changed but not added to ingressRouteURLs", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).ToNot(HaveOccurred()) - oldObj.Spec.IngressRouteURLs = nil - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: oldObj.Spec.Service.URL}} - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("must contain baseURL: %s", obj.URL()), - )))) - Expect(warnings).To(BeEmpty()) - - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} - warnings, err = validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("must contain baseURL: %s", oldObj.URL()), - )))) - Expect(warnings).To(BeEmpty()) - - }) - - It("Should deny update if a label was removed", func() { - oldKey := "" - for label := range obj.Labels { - oldKey = label - delete(obj.Labels, label) - break - } - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("metadata").Child("labels").Child(oldKey), - "labels cannot be removed", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a label changed", func() { - oldKey := "" - oldValue := "" - newValue := "" - for label, val := range obj.Labels { - oldKey = label - oldValue = val - newValue = val + "-newval" - obj.Labels[label] = newValue - break - } - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("metadata").Child("labels").Child(oldKey), - newValue, - "immutable: should be: "+oldValue, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a label was added", func() { - newKey := "new-label" - obj.Labels[newKey] = "test" - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("metadata").Child("labels").Child(newKey), - "new labels cannot be added", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if an inspire block was added", func() { - obj.Spec.Service.Inspire = &pdoknlv3.WFSInspire{} - oldObj.Spec.Service.Inspire = nil - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("spec").Child("service").Child("inspire"), - "cannot change from inspire to not inspire or the other way around", - )))) - Expect(warnings).To(BeEmpty()) - }) + // TODO (user): Add logic for validating webhooks + // Example: + // It("Should deny creation if a required field is missing", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "" + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + // }) + // + // It("Should admit creation if all required fields are present", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "valid_value" + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) + // }) + // + // It("Should validate updates correctly", func() { + // By("simulating a valid update scenario") + // oldObj.SomeRequiredField = "updated_value" + // obj.SomeRequiredField = "updated_value" + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) + // }) + }) - It("Should deny update if an inspire block was removed", func() { - oldObj.Spec.Service.Inspire = &pdoknlv3.WFSInspire{} - obj.Spec.Service.Inspire = nil - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("spec").Child("service").Child("inspire"), - "cannot change from inspire to not inspire or the other way around", - )))) - Expect(warnings).To(BeEmpty()) - }) + Context("When creating WFS under Conversion Webhook", func() { + // TODO (user): Add logic to convert the object to the desired version and verify the conversion + // Example: + // It("Should convert the object correctly", func() { + // convertedObj := &pdoknlv3.WFS{} + // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) + // Expect(convertedObj).ToNot(BeNil()) + // }) }) }) diff --git a/internal/webhook/v3/wms_webhook.go b/internal/webhook/v3/wms_webhook.go index d5befee..e5a846a 100644 --- a/internal/webhook/v3/wms_webhook.go +++ b/internal/webhook/v3/wms_webhook.go @@ -1,106 +1,36 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ -//nolint:dupl package v3 import ( - "context" - "fmt" - - "sigs.k8s.io/controller-runtime/pkg/client" - - "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) +// nolint:unused // log is for logging in this package. -var wmsLog = logf.Log.WithName("wms-resource") +var wmslog = logf.Log.WithName("wms-resource") // SetupWMSWebhookWithManager registers the webhook for WMS in the manager. func SetupWMSWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr).For(&pdoknlv3.WMS{}). - WithValidator(&WMSCustomValidator{mgr.GetClient()}). Complete() } -// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. -// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. -// +kubebuilder:webhook:path=/validate-pdok-nl-v3-wms,mutating=false,failurePolicy=fail,sideEffects=None,groups=pdok.nl,resources=wms,verbs=create;update,versions=v3,name=vwms-v3.kb.io,admissionReviewVersions=v1 - -// WMSCustomValidator struct is responsible for validating the WMS resource -// when it is created, updated, or deleted. -// -// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, -// as this struct is used only for temporary operations and does not need to be deeply copied. -type WMSCustomValidator struct { - Client client.Client -} - -var _ webhook.CustomValidator = &WMSCustomValidator{} - -// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type WMS. -func (v *WMSCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { - wms, ok := obj.(*pdoknlv3.WMS) - if !ok { - return nil, fmt.Errorf("expected a WMS object but got %T", obj) - } - wmsLog.Info("Validation for WMS upon creation", "name", wms.GetName()) - - return wms.ValidateCreate(v.Client) -} - -// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type WMS. -func (v *WMSCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { - wms, ok := newObj.(*pdoknlv3.WMS) - if !ok { - return nil, fmt.Errorf("expected a WMS object for the newObj but got %T", newObj) - } - wmsOld, ok := oldObj.(*pdoknlv3.WMS) - if !ok { - return nil, fmt.Errorf("expected a WMS object for the oldObj but got %T", newObj) - } - wmsLog.Info("Validation for WMS upon update", "name", wms.GetName()) - - return wms.ValidateUpdate(v.Client, wmsOld) -} - -// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type WMS. -func (v *WMSCustomValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { - wms, ok := obj.(*pdoknlv3.WMS) - if !ok { - return nil, fmt.Errorf("expected a WMS object but got %T", obj) - } - wmsLog.Info("Validation for WMS upon deletion", "name", wms.GetName()) - - // TODO(user): fill in your validation logic upon object deletion. - - return nil, nil -} +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! diff --git a/internal/webhook/v3/wms_webhook_test.go b/internal/webhook/v3/wms_webhook_test.go index eb54622..701c32b 100644 --- a/internal/webhook/v3/wms_webhook_test.go +++ b/internal/webhook/v3/wms_webhook_test.go @@ -1,677 +1,55 @@ /* -MIT License +Copyright 2025. -Copyright (c) 2024 Publieke Dienstverlening op de Kaart +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ package v3 -//nolint:revive // Complains about the dot imports import ( - "context" - "fmt" - - smoothoperatormodel "github.com/pdok/smooth-operator/model" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/utils/ptr" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + // TODO (user): Add any additional imports if needed ) var _ = Describe("WMS Webhook", func() { var ( - obj *pdoknlv3.WMS - oldObj *pdoknlv3.WMS - validator WMSCustomValidator - ownerInfo *smoothoperatorv1.OwnerInfo + obj *pdoknlv3.WMS + oldObj *pdoknlv3.WMS ) BeforeEach(func() { - validator = WMSCustomValidator{k8sClient} - Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") - - sample := &pdoknlv3.WMS{} - Expect(readSample(sample)).To(Succeed(), "Reading and parsing the WMS V3 sample failed") - - obj = sample.DeepCopy() - oldObj = sample.DeepCopy() - - Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + obj = &pdoknlv3.WMS{} + oldObj = &pdoknlv3.WMS{} Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") - - ownerInfoSample := &smoothoperatorv1.OwnerInfo{} - Expect(readOwnerInfo(ownerInfoSample)).To(Succeed(), "Reading and parsing the Ownerinfo sample failed") - ownerInfo = ownerInfoSample.DeepCopy() - Expect(ownerInfo).NotTo(BeNil()) - Expect(createOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests }) AfterEach(func() { - Expect(k8sClient.Delete(ctx, ownerInfo)).To(Succeed()) + // TODO (user): Add any teardown logic common to all tests }) - Context("When creating or updating WMS under Conversion Webhook", func() { - ctx := context.Background() - - It("Creates the WMS from the sample", func() { - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(BeEmpty()) - }) - - It("Should Deny Create when Labels are empty", func() { - obj.Labels = nil - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("metadata").Child("labels"), - "can't be empty", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when URL not in IngressRouteURLs", func() { - url, err := smoothoperatormodel.ParseURL("http://changed/changed") - Expect(err).To(BeNil()) - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} - url, err = smoothoperatormodel.ParseURL("http://sample/sample") - Expect(err).To(BeNil()) - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("must contain baseURL: %s", url), - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Warns when the name contains WMS", func() { - obj.Name += "-wms" - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("metadata").Child("name"), - "name should not contain wms", - []string{}, - ))) - }) - - It("Warns when mapfile and resolution are set", func() { - withMapfile(obj) - obj.Spec.Service.Resolution = ptr.To(int32(5)) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("resolution"), - "not used when service.mapfile is configured", - []string{}, - ))) - }) - - It("Warns when mapfile and defResolution are set", func() { - withMapfile(obj) - obj.Spec.Service.DefResolution = ptr.To(int32(5)) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("defResolution"), - "not used when service.mapfile is configured", - []string{}, - ))) - }) - - It("Should deny Create when URL not in IngressRouteURLs", func() { - obj.Spec.Service.Inspire = &pdoknlv3.Inspire{ServiceMetadataURL: pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}}} - obj.Spec.Service.Layer.Layers[0].DatasetMetadataURL = &pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), - "metadata", - "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as an datasetMetadataUrl.csw.metadataIdentifier", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when minReplicas are larger than maxReplicas", func() { - obj.Spec.HorizontalPodAutoscalerPatch = &pdoknlv3.HorizontalPodAutoscalerPatch{ - MinReplicas: ptr.To(int32(10)), - MaxReplicas: ptr.To(int32(5)), - } - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("horizontalPodAutoscaler"), - fmt.Sprintf("minReplicas: %d, maxReplicas: %d", 10, 5), - "maxReplicas cannot be less than minReplicas", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when mapserver container doesn't have ephemeral storage", func() { - obj.Spec.PodSpecPatch = corev1.PodSpec{} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required(field.NewPath("spec"). - Child("podSpecPatch"). - Child("containers"). - Key("mapserver"). - Child("resources"). - Child("limits"). - Child(corev1.ResourceEphemeralStorage.String()), "")))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when multiple layers have the same name", func() { - layerName := "equal" - obj.Spec.Service.Layer.Layers[0].Name = &layerName - obj.Spec.Service.Layer.Layers[1].Name = &layerName - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Duplicate( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("name"), - layerName, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when Group Layer has data set", func() { - data := pdoknlv3.Data{} - obj.Spec.Service.Layer.Layers[1].Data = &data - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("data"), - data, - "must not be set on a GroupLayer", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Warns when mapfile and layer boundingboxes are both set", func() { - withMapfile(obj) - obj.Spec.Service.Layer.BoundingBoxes = []pdoknlv3.WMSBoundingBox{{ - CRS: "", - BBox: smoothoperatormodel.BBox{}, - }} - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("boundingBoxes"), - "is not used when service.mapfile is configured", - []string{}, - ))) - }) - - It("Should deny Create when there is no layer boundingbox set for dataepsg and no custom mapfile", func() { - obj.Spec.Service.Mapfile = nil - obj.Spec.Service.DataEPSG = "EPSG:1234" - obj.Spec.Service.Layer.Layers = []pdoknlv3.Layer{obj.Spec.Service.Layer.Layers[0]} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("boundingBoxes").Child("crs"), - fmt.Sprintf("must contain a boundingBox for CRS %s when service.dataEPSG is not 'EPSG:28992'", obj.Spec.Service.DataEPSG), - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Warns when unused fields are set on a tiff connection when using a custom mapfile", func() { - withMapfile(obj) - obj.Spec.Service.Layer.Layers[0].Data = &pdoknlv3.Data{TIF: &pdoknlv3.TIF{ - BlobKey: "blobkey", - Resample: "AVERAGE", - Offsite: ptr.To("offsite"), - GetFeatureInfoIncludesClass: true, - }} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("data").Child("tif").Child("getFeatureInfoIncludesClass"), - "is not used when service.mapfile is configured", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("data").Child("tif").Child("offsite"), - "is not used when service.mapfile is configured", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("data").Child("tif").Child("resample"), - "is not used when service.mapfile is configured", - []string{}, - ))))) - }) - - It("Should deny Create when there is a Group Layer that is not visible", func() { - obj.Spec.Service.Layer.Layers[1].Visible = false - obj.Spec.Service.Layer.Layers[1].Title = nil - obj.Spec.Service.Layer.Layers[1].Abstract = nil - obj.Spec.Service.Layer.Layers[1].Keywords = nil - obj.Spec.Service.Layer.Layers[1].Styles[0].Title = nil - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("visible"), - false, - "must be true for a "+pdoknlv3.GroupLayer, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Warns when unused fields are set on a layer that is not visible", func() { - obj.Spec.Service.Layer.Layers[0].Visible = false - obj.Spec.Service.Layer.Layers[0].Title = ptr.To("title") - obj.Spec.Service.Layer.Layers[0].Abstract = ptr.To("abstract") - obj.Spec.Service.Layer.Layers[0].Keywords = []string{"keyword"} - obj.Spec.Service.Layer.Layers[0].DatasetMetadataURL = &pdoknlv3.MetadataURL{} - obj.Spec.Service.Layer.Layers[0].Authority = &pdoknlv3.Authority{} - obj.Spec.Service.Layer.Layers[0].Styles[0].Title = ptr.To("title") - obj.Spec.Service.Layer.Layers[0].Styles[0].Abstract = ptr.To("abstract") - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(Equal(getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("abstract"), - "is not used when layer.visible=false", getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("title"), - "is not used when layer.visible=false", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("authority"), - "is not used when layer.visible=false", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("datasetMetadataURL"), - "is not used when layer.visible=false", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("keywords"), - "is not used when layer.visible=false", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("abstract"), - "is not used when layer.visible=false", - getValidationWarnings( - obj, - *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("title"), - "is not used when layer.visible=false", - []string{}, - ))))))))) - }) - - It("Should deny Create when a Layer has multiple boundingBoxes with the same CRS", func() { - bbox := pdoknlv3.WMSBoundingBox{ - CRS: "EPSG:28992", - BBox: smoothoperatormodel.BBox{ - MinX: "-25000", - MinY: "250000", - MaxX: "280000", - MaxY: "860000", - }, - } - obj.Spec.Service.Layer.BoundingBoxes = []pdoknlv3.WMSBoundingBox{bbox, bbox} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Duplicate( - field.NewPath("spec").Child("service").Child("layer").Child("boundingBoxes").Index(1).Child("crs"), - bbox.CRS, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a Layer uses the same style name multiple times", func() { - styleName := "duplicate" - style := pdoknlv3.Style{ - Name: styleName, - Title: ptr.To("Title"), - Visualization: obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization, - } - obj.Spec.Service.Layer.Layers[0].Styles = []pdoknlv3.Style{style, style} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(1).Child("name"), - styleName, - "A Layer can't use the same style name multiple times", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a Style doesn't have a title on its highest visible layer", func() { - obj.Spec.Service.Layer.Layers[1].Styles[0].Title = nil - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("styles").Index(0).Child("title"), - "A Style must have a title on the highest visible Layer", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a GroupLayer Style uses the same name as a Style from a parent Layer", func() { - styleName := "duplicate" - obj.Spec.Service.Layer.Styles = []pdoknlv3.Style{{Name: styleName, Title: ptr.To("title")}} - obj.Spec.Service.Layer.Layers[1].Styles = []pdoknlv3.Style{{Name: styleName, Title: ptr.To("title")}} - obj.Spec.Service.Layer.Layers[0].Styles[0].Name = styleName - obj.Spec.Service.Layer.Layers[1].Layers[0].Styles[0].Name = styleName - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("styles").Index(0).Child("name"), - styleName, - "A GroupLayer can't redefine the same style as a parent layer", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a GroupLayer Style has visualization", func() { - visualization := "file.style" - obj.Spec.Service.Layer.Layers[1].Styles[0].Visualization = &visualization - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("styles").Index(0).Child("visualization"), - visualization, - "GroupLayers must not have a visualization", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a Style has a visualization while a custom mapfile is configured", func() { - visualization := "file.style" - withMapfile(obj) - obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization = &visualization - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("visualization"), - visualization, - "is not used when spec.service.mapfile is used", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a Data Layer has a Style with no visualization while a no custom mapfile is configured", func() { - obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization = nil - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("visualization"), - "on DataLayers when spec.service.mapfile is not used", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a when a Visualization file is not defined in the stylingassets", func() { - visualization := "new.style" - obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization = &visualization - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("visualization"), - visualization, - "must be defined be in spec.service.stylingAssets.configMapKeyRefs.Keys", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when a when a Group Layer style isn't implemented in a sub Data Layer", func() { - obj.Spec.Service.Layer.Layers[1].Styles[0].Name = "new" - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("layers").Index(0).Child("styles"), - nil, - fmt.Sprintf("dataLayer must implement style: %s, defined by a parent layer", "new"), - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny Create when there are no visible layers", func() { - obj.Spec.Service.Layer.Layers = []pdoknlv3.Layer{obj.Spec.Service.Layer.Layers[1].Layers[0]} - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("layer").Child("layers[*]").Child("visible"), - "at least one layer must be visible", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef doesn't exist", func() { - obj.Spec.Service.OwnerInfoRef = "changed" - - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.NotFound( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "changed", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef misses namespaceTemplate", func() { - ownerInfo.Spec.NamespaceTemplate = nil - - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.namespaceTemplate missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef misses csw metadataTemplate", func() { - obj.Spec.Service.Inspire = &pdoknlv3.Inspire{ServiceMetadataURL: pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}}} - ownerInfo.Spec.MetadataUrls.CSW = nil - - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.metadataUrls.csw missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - - ownerInfo.Spec.MetadataUrls = nil - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err = validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.metadataUrls.csw missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny create if the OwnerInfoRef misses WMS", func() { - ownerInfo.Spec.WMS = nil - Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) - warnings, err := validator.ValidateCreate(ctx, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("spec").Child("service").Child("ownerInfoRef"), - "spec.WMS missing in "+ownerInfo.Name, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a ingressRouteURL was removed", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).To(BeNil()) - oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ - {URL: obj.URL()}, - {URL: smoothoperatormodel.URL{URL: url}}, - } - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: obj.URL()}} - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("urls cannot be removed, missing: %s", smoothoperatormodel.IngressRouteURL{URL: smoothoperatormodel.URL{URL: url}}), - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should accept update if a url was changed when it's in ingressRouteUrls", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).To(BeNil()) - oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ - {URL: obj.URL()}, - {URL: smoothoperatormodel.URL{URL: url}}, - } - obj.Spec.IngressRouteURLs = oldObj.Spec.IngressRouteURLs - oldObj.Spec.Service.URL = obj.URL() - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(BeNil()) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a url was changed and ingressRouteUrls = nil", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).To(BeNil()) - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - obj.Spec.IngressRouteURLs = nil - oldObj.Spec.IngressRouteURLs = nil - - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("spec").Child("service").Child("url"), - "is immutable, add the old and new urls to spec.ingressRouteUrls in order to change this field", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update url was changed but not added to ingressRouteURLs", func() { - url, err := smoothoperatormodel.ParseURL("http://new.url/path") - Expect(err).ToNot(HaveOccurred()) - oldObj.Spec.IngressRouteURLs = nil - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: oldObj.Spec.Service.URL}} - obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("must contain baseURL: %s", obj.URL()), - )))) - Expect(warnings).To(BeEmpty()) - - obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} - warnings, err = validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("spec").Child("ingressRouteUrls"), - fmt.Sprint(obj.Spec.IngressRouteURLs), - fmt.Sprintf("must contain baseURL: %s", oldObj.URL()), - )))) - Expect(warnings).To(BeEmpty()) - - }) - - It("Should deny update if a label was removed", func() { - oldKey := "" - for label := range obj.Labels { - oldKey = label - delete(obj.Labels, label) - break - } - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Required( - field.NewPath("metadata").Child("labels").Child(oldKey), - "labels cannot be removed", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a label changed", func() { - oldKey := "" - oldValue := "" - newValue := "" - for label, val := range obj.Labels { - oldKey = label - oldValue = val - newValue = val + "-newval" - obj.Labels[label] = newValue - break - } - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Invalid( - field.NewPath("metadata").Child("labels").Child(oldKey), - newValue, - "immutable: should be: "+oldValue, - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if a label was added", func() { - newKey := "new-label" - obj.Labels[newKey] = "test" - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("metadata").Child("labels").Child(newKey), - "new labels cannot be added", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if an inspire block was added", func() { - obj.Spec.Service.Inspire = &pdoknlv3.Inspire{} - oldObj.Spec.Service.Inspire = nil - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("spec").Child("service").Child("inspire"), - "cannot change from inspire to not inspire or the other way around", - )))) - Expect(warnings).To(BeEmpty()) - }) - - It("Should deny update if an inspire block was removed", func() { - oldObj.Spec.Service.Inspire = &pdoknlv3.Inspire{} - obj.Spec.Service.Inspire = nil - warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) - Expect(err).To(Equal(getValidationError(obj, field.Forbidden( - field.NewPath("spec").Child("service").Child("inspire"), - "cannot change from inspire to not inspire or the other way around", - )))) - Expect(warnings).To(BeEmpty()) - }) - + Context("When creating WMS under Conversion Webhook", func() { + // TODO (user): Add logic to convert the object to the desired version and verify the conversion + // Example: + // It("Should convert the object correctly", func() { + // convertedObj := &pdoknlv3.WMS{} + // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) + // Expect(convertedObj).ToNot(BeNil()) + // }) }) }) - -func withMapfile(wms *pdoknlv3.WMS) { - wms.Spec.Service.Mapfile = &pdoknlv3.Mapfile{} - wms.Spec.Service.Layer.Layers[0].Styles[0].Visualization = nil - wms.Spec.Service.Layer.Layers[1].Layers[0].Styles[0].Visualization = nil -} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index ed2d505..cfb69c0 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -16,7 +16,6 @@ limitations under the License. package e2e -//nolint:revive // Complains about the dot imports import ( "fmt" "os" @@ -31,14 +30,10 @@ import ( var ( // Optional Environment Variables: - // - PROMETHEUS_INSTALL_SKIP=true: Skips Prometheus Operator installation during test setup. // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. - // These variables are useful if Prometheus or CertManager is already installed, avoiding + // These variables are useful if CertManager is already installed, avoiding // re-installation and conflicts. - skipPrometheusInstall = os.Getenv("PROMETHEUS_INSTALL_SKIP") == "true" skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" - // isPrometheusOperatorAlreadyInstalled will be set true when prometheus CRDs be found on the cluster - isPrometheusOperatorAlreadyInstalled = false // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster isCertManagerAlreadyInstalled = false @@ -50,7 +45,7 @@ var ( // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, // temporary environment to validate project changes with the the purposed to be used in CI jobs. // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs -// CertManager and Prometheus. +// CertManager. func TestE2E(t *testing.T) { RegisterFailHandler(Fail) _, _ = fmt.Fprintf(GinkgoWriter, "Starting mapserver-operator integration test suite\n") @@ -58,11 +53,8 @@ func TestE2E(t *testing.T) { } var _ = BeforeSuite(func() { - By("Ensure that Prometheus is enabled") - _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") - By("building the manager(Operator) image") - cmd := exec.Command("make", "docker-build", "IMG="+projectImage) + cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage)) _, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image") @@ -73,19 +65,9 @@ var _ = BeforeSuite(func() { ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. - // To prevent errors when tests run in environments with Prometheus or CertManager already installed, - // we check for their presence before execution. - // Setup Prometheus and CertManager before the suite if not skipped and if not already installed - if !skipPrometheusInstall { - By("checking if prometheus is installed already") - isPrometheusOperatorAlreadyInstalled = utils.IsPrometheusCRDsInstalled() - if !isPrometheusOperatorAlreadyInstalled { - _, _ = fmt.Fprintf(GinkgoWriter, "Installing Prometheus Operator...\n") - Expect(utils.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") - } else { - _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: Prometheus Operator is already installed. Skipping installation...\n") - } - } + // To prevent errors when tests run in environments with CertManager already installed, + // we check for its presence before execution. + // Setup CertManager before the suite if not skipped and if not already installed if !skipCertManagerInstall { By("checking if cert manager is installed already") isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() @@ -99,11 +81,7 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - // Teardown Prometheus and CertManager after the suite if not skipped and if they were not already installed - if !skipPrometheusInstall && !isPrometheusOperatorAlreadyInstalled { - _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Prometheus Operator...\n") - utils.UninstallPrometheusOperator() - } + // Teardown CertManager after the suite if not skipped and if it was not already installed if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") utils.UninstallCertManager() diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 9989296..e46a748 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -16,10 +16,12 @@ limitations under the License. package e2e -//nolint:revive // Complains about the dot imports import ( + "encoding/json" "fmt" + "os" "os/exec" + "path/filepath" "time" . "github.com/onsi/ginkgo/v2" @@ -31,6 +33,15 @@ import ( // namespace where the project is deployed in const namespace = "mapserver-operator-system" +// serviceAccountName created for the project +const serviceAccountName = "mapserver-operator-controller-manager" + +// metricsServiceName is the name of the metrics service of the project +const metricsServiceName = "mapserver-operator-controller-manager-metrics-service" + +// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data +const metricsRoleBindingName = "mapserver-operator-metrics-binding" + var _ = Describe("Manager", Ordered, func() { var controllerPodName string @@ -55,7 +66,7 @@ var _ = Describe("Manager", Ordered, func() { Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") By("deploying the controller-manager") - cmd = exec.Command("make", "deploy", "IMG="+projectImage) + cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") }) @@ -158,5 +169,213 @@ var _ = Describe("Manager", Ordered, func() { } Eventually(verifyControllerUp).Should(Succeed()) }) + + It("should ensure the metrics endpoint is serving metrics", func() { + By("creating a ClusterRoleBinding for the service account to allow access to metrics") + cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, + "--clusterrole=mapserver-operator-metrics-reader", + fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), + ) + _, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") + + By("validating that the metrics service is available") + cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") + + By("getting the service account token") + token, err := serviceAccountToken() + Expect(err).NotTo(HaveOccurred()) + Expect(token).NotTo(BeEmpty()) + + By("waiting for the metrics endpoint to be ready") + verifyMetricsEndpointReady := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready") + } + Eventually(verifyMetricsEndpointReady).Should(Succeed()) + + By("verifying that the controller manager is serving the metrics server") + verifyMetricsServerStarted := func(g Gomega) { + cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), + "Metrics server not yet started") + } + Eventually(verifyMetricsServerStarted).Should(Succeed()) + + By("creating the curl-metrics pod to access the metrics endpoint") + cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", + "--namespace", namespace, + "--image=curlimages/curl:latest", + "--overrides", + fmt.Sprintf(`{ + "spec": { + "containers": [{ + "name": "curl", + "image": "curlimages/curl:latest", + "command": ["/bin/sh", "-c"], + "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], + "securityContext": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + }, + "runAsNonRoot": true, + "runAsUser": 1000, + "seccompProfile": { + "type": "RuntimeDefault" + } + } + }], + "serviceAccount": "%s" + } + }`, token, metricsServiceName, namespace, serviceAccountName)) + _, err = utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") + + By("waiting for the curl-metrics pod to complete.") + verifyCurlUp := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", + "-o", "jsonpath={.status.phase}", + "-n", namespace) + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") + } + Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) + + By("getting the metrics by checking curl-metrics logs") + metricsOutput := getMetricsOutput() + Expect(metricsOutput).To(ContainSubstring( + "controller_runtime_reconcile_total", + )) + }) + + It("should provisioned cert-manager", func() { + By("validating that cert-manager has the certificate Secret") + verifyCertManager := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) + _, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + } + Eventually(verifyCertManager).Should(Succeed()) + }) + + It("should have CA injection for WMS conversion webhook", func() { + By("checking CA injection for WMS conversion webhook") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "customresourcedefinitions.apiextensions.k8s.io", + "wms..pdok.nl", + "-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}") + vwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + It("should have CA injection for validating webhooks", func() { + By("checking CA injection for validating webhooks") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "validatingwebhookconfigurations.admissionregistration.k8s.io", + "mapserver-operator-validating-webhook-configuration", + "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") + vwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + It("should have CA injection for WFS conversion webhook", func() { + By("checking CA injection for WFS conversion webhook") + verifyCAInjection := func(g Gomega) { + cmd := exec.Command("kubectl", "get", + "customresourcedefinitions.apiextensions.k8s.io", + "wfs..pdok.nl", + "-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}") + vwhOutput, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) + } + Eventually(verifyCAInjection).Should(Succeed()) + }) + + // +kubebuilder:scaffold:e2e-webhooks-checks + + // TODO: Customize the e2e test suite with scenarios specific to your project. + // Consider applying sample/CR(s) and check their status and/or verifying + // the reconciliation by using the metrics, i.e.: + // metricsOutput := getMetricsOutput() + // Expect(metricsOutput).To(ContainSubstring( + // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, + // strings.ToLower(), + // )) }) }) + +// serviceAccountToken returns a token for the specified service account in the given namespace. +// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request +// and parsing the resulting token from the API response. +func serviceAccountToken() (string, error) { + const tokenRequestRawString = `{ + "apiVersion": "authentication.k8s.io/v1", + "kind": "TokenRequest" + }` + + // Temporary file to store the token request + secretName := fmt.Sprintf("%s-token-request", serviceAccountName) + tokenRequestFile := filepath.Join("/tmp", secretName) + err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) + if err != nil { + return "", err + } + + var out string + verifyTokenCreation := func(g Gomega) { + // Execute kubectl command to create the token + cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( + "/api/v1/namespaces/%s/serviceaccounts/%s/token", + namespace, + serviceAccountName, + ), "-f", tokenRequestFile) + + output, err := cmd.CombinedOutput() + g.Expect(err).NotTo(HaveOccurred()) + + // Parse the JSON output to extract the token + var token tokenRequest + err = json.Unmarshal(output, &token) + g.Expect(err).NotTo(HaveOccurred()) + + out = token.Status.Token + } + Eventually(verifyTokenCreation).Should(Succeed()) + + return out, err +} + +// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. +func getMetricsOutput() string { + By("getting the curl-metrics logs") + cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) + metricsOutput, err := utils.Run(cmd) + Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") + Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) + return metricsOutput +} + +// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, +// containing only the token field that we need to extract. +type tokenRequest struct { + Status struct { + Token string `json:"token"` + } `json:"status"` +} diff --git a/test/utils/utils.go b/test/utils/utils.go index fef95da..04a5141 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -54,7 +54,7 @@ func Run(cmd *exec.Cmd) (string, error) { _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) output, err := cmd.CombinedOutput() if err != nil { - return string(output), fmt.Errorf("%s failed with error: (%w) %s", command, err, string(output)) + return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) } return string(output), nil @@ -197,7 +197,7 @@ func GetProjectDir() (string, error) { if err != nil { return wd, err } - wd = strings.ReplaceAll(wd, "/test/e2e", "") + wd = strings.Replace(wd, "/test/e2e", "", -1) return wd, nil } @@ -205,6 +205,7 @@ func GetProjectDir() (string, error) { // of the target content. The target content may span multiple lines. func UncommentCode(filename, target, prefix string) error { // false positive + // nolint:gosec content, err := os.ReadFile(filename) if err != nil { return err @@ -245,6 +246,6 @@ func UncommentCode(filename, target, prefix string) error { return err } // false positive - //nolint:gosec + // nolint:gosec return os.WriteFile(filename, out.Bytes(), 0644) } From b218c63565e66a960a252e77502b371e0543747d Mon Sep 17 00:00:00 2001 From: Jelle Dijkstra Date: Mon, 22 Dec 2025 16:07:51 +0100 Subject: [PATCH 2/7] (chore) original code from master to keep changes --- .devcontainer/devcontainer.json | 2 +- .github/workflows/build-and-publish-image.yml | 67 + .github/workflows/lint.yml | 6 +- .github/workflows/test-e2e.yml | 4 +- .github/workflows/test.yml | 4 +- .golangci.yml | 143 +- DEVELOPMENT.md | 5 + Dockerfile | 6 +- LICENSE | 21 + Makefile | 11 +- README.md | 107 +- api/v2beta1/groupversion_info.go | 34 +- api/v2beta1/shared_conversion.go | 289 +++ api/v2beta1/shared_types.go | 145 ++ api/v2beta1/wfs_conversion.go | 269 ++- api/v2beta1/wfs_types.go | 87 +- api/v2beta1/wms_conversion.go | 673 ++++++- api/v2beta1/wms_conversion_test.go | 232 +++ api/v2beta1/wms_types.go | 127 +- api/v2beta1/zz_generated.deepcopy.go | 796 +++++++- api/v3/groupversion_info.go | 45 +- api/v3/shared_types.go | 329 +++ api/v3/shared_validation.go | 210 ++ api/v3/wfs_conversion.go | 28 +- api/v3/wfs_types.go | 309 ++- api/v3/wfs_validation.go | 101 + api/v3/wms_conversion.go | 28 +- api/v3/wms_types.go | 697 ++++++- api/v3/wms_types_test.go | 166 ++ api/v3/wms_validation.go | 334 +++ api/v3/zz_generated.deepcopy.go | 883 +++++++- build-push-deploy-locally.sh | 36 + cmd/main.go | 220 +- config/crd/bases/embed.go | 51 + config/crd/bases/pdok.nl_wfs.yaml | 1185 +++++++++++ config/crd/bases/pdok.nl_wms.yaml | 1782 +++++++++++++++++ config/crd/kustomization.yaml | 6 +- config/crd/update_openapi.go | 180 ++ config/default/kustomization.yaml | 406 ++-- config/manager/kustomization.yaml | 6 + config/manager/manager.yaml | 2 + config/prometheus/monitor_tls_patch.yaml | 37 +- config/rbac/role.yaml | 99 + config/samples/samples.go | 17 + config/samples/v2beta1_wfs.yaml | 58 +- config/samples/v3_wfs.yaml | 41 +- config/samples/v3_wms.yaml | 69 +- config/webhook/manifests.yaml | 20 + go.mod | 180 +- go.sum | 377 ++-- hack/boilerplate.go.txt | 28 +- .../controller/blobdownload/blob_download.go | 225 +++ .../blobdownload/blob_download_test.go | 332 +++ .../controller/blobdownload/gpkg_download.sh | 175 ++ .../capabilities_generator.go | 76 + .../capabilities_generator_test.go | 220 ++ .../capabilitiesgenerator/default_bboxes.go | 132 ++ .../capabilitiesgenerator/mapper.go | 691 +++++++ .../test_data/wfs_input.yaml | 78 + .../test_data/wms_input.yaml | 407 ++++ internal/controller/configmaps.go | 147 ++ internal/controller/configmaps_test.go | 31 + internal/controller/constants/constants.go | 29 + internal/controller/deployment.go | 296 +++ .../featureinfo_generator.go | 49 + .../featureinfo_generator_test.go | 142 ++ .../controller/featureinfogenerator/mapper.go | 69 + .../controller/horizontalpodautoscaler.go | 97 + internal/controller/ingressroute.go | 161 ++ .../legendgenerator/legend-fixer.sh | 37 + .../legendgenerator/legend_generator.go | 85 + .../legendgenerator/legend_generator_test.go | 42 + internal/controller/legendgenerator/mapper.go | 140 ++ .../test_data/expected/legend-fix.yaml | 121 ++ .../test_data/expected/no-legend-fix.yaml | 14 + .../test_data/input/legend-fix.yaml | 527 +++++ .../test_data/input/no-legend-fix.yaml | 193 ++ .../mapfilegenerator/mapfile_generator.go | 84 + .../mapfile_generator_test.go | 110 + .../controller/mapfilegenerator/mapper.go | 325 +++ .../test_data/expected/wfs.json | 73 + .../test_data/expected/wms_group.json | 468 +++++ .../expected/wms_group_and_toplayer.json | 616 ++++++ .../test_data/expected/wms_groupless.json | 283 +++ .../test_data/expected/wms_postgis.json | 93 + .../test_data/expected/wms_tif.json | 306 +++ .../mapfilegenerator/test_data/input/wfs.yaml | 94 + .../test_data/input/wms_group.yaml | 576 ++++++ .../input/wms_group_and_toplayer.yaml | 783 ++++++++ .../test_data/input/wms_groupless.yaml | 298 +++ .../test_data/input/wms_postgis.yaml | 185 ++ .../test_data/input/wms_tif.yaml | 438 ++++ internal/controller/mapfilegenerator/types.go | 136 ++ internal/controller/mapperutils/utils.go | 72 + internal/controller/mapserver/deployment.go | 200 ++ .../controller/mapserver/deployment_test.go | 69 + .../test_data/expected_livenessprobe.yaml | 11 + .../test_data/expected_readinessprobe.yaml | 11 + .../test_data/expected_startupprobe.yaml | 11 + .../test_data/expected_volumemounts.yaml | 18 + .../mapserver/test_data/v2_input.yaml | 162 ++ internal/controller/middleware.go | 48 + .../ogc_webservice_proxy.go | 93 + .../ogc_webservice_proxy_test.go | 46 + .../test_data/expected/named-toplayer.yaml | 12 + .../test_data/expected/unnamed-toplayer.yaml | 7 + .../test_data/input/named-toplayer.yaml | 52 + .../test_data/input/unnamed-toplayer.yaml | 51 + internal/controller/poddisruptionbudget.go | 42 + internal/controller/reconciler.go | 45 + internal/controller/service.go | 77 + internal/controller/shared_controller.go | 306 +++ internal/controller/shared_controller_test.go | 456 +++++ .../static/files/default_mapserver.conf | 8 + internal/controller/static/files/include.conf | 15 + internal/controller/static/files/ogc.lua | 83 + .../static/files/scraping-error.xml | 8 + internal/controller/static/reader.go | 26 + internal/controller/suite_test.go | 166 +- .../configmap-capabilities-generator.yaml | 118 ++ .../expected/configmap-init-scripts.yaml | 190 ++ .../expected/configmap-mapfile-generator.yaml | 122 ++ .../expected/configmap-mapserver.yaml | 143 ++ .../wfs/complete/expected/deployment.yaml | 270 +++ .../expected/horizontalpodautoscaler.yaml | 52 + .../wfs/complete/expected/ingressroute.yaml | 43 + .../complete/expected/middleware-headers.yaml | 27 + .../expected/poddisruptionbudget.yaml | 31 + .../wfs/complete/expected/service.yaml | 41 + .../wfs/complete/input/ownerinfo.yaml | 23 + .../test_data/wfs/complete/input/wfs.yaml | 166 ++ .../configmap-capabilities-generator.yaml | 22 + .../expected/configmap-init-scripts.yaml | 22 + .../expected/configmap-mapfile-generator.yaml | 22 + .../minimal/expected/configmap-mapserver.yaml | 26 + .../wfs/minimal/expected/deployment.yaml | 253 +++ .../expected/horizontalpodautoscaler.yaml | 52 + .../wfs/minimal/expected/ingressroute.yaml | 35 + .../minimal/expected/middleware-headers.yaml | 26 + .../minimal/expected/poddisruptionbudget.yaml | 29 + .../wfs/minimal/expected/service.yaml | 40 + .../wfs/minimal/input/ownerinfo.yaml | 23 + .../test_data/wfs/minimal/input/wfs.yaml | 71 + .../configmap-capabilities-generator.yaml | 22 + .../expected/configmap-mapfile-generator.yaml | 22 + .../expected/configmap-mapserver.yaml | 26 + .../wfs/noprefetch/expected/deployment.yaml | 245 +++ .../expected/horizontalpodautoscaler.yaml | 52 + .../wfs/noprefetch/expected/ingressroute.yaml | 35 + .../expected/middleware-headers.yaml | 26 + .../expected/poddisruptionbudget.yaml | 29 + .../wfs/noprefetch/expected/service.yaml | 40 + .../wfs/noprefetch/input/ownerinfo.yaml | 23 + .../test_data/wfs/noprefetch/input/wfs.yaml | 72 + .../configmap-capabilities-generator.yaml | 244 +++ .../configmap-featureinfo-generator.yaml | 72 + .../expected/configmap-init-scripts.yaml | 190 ++ .../expected/configmap-legend-generator.yaml | 86 + .../expected/configmap-mapfile-generator.yaml | 179 ++ .../expected/configmap-mapserver.yaml | 147 ++ .../configmap-ogc-webservice-proxy.yaml | 32 + .../wms/complete/expected/deployment.yaml | 404 ++++ .../expected/horizontalpodautoscaler.yaml | 53 + .../wms/complete/expected/ingressroute.yaml | 62 + .../complete/expected/middleware-headers.yaml | 27 + .../expected/poddisruptionbudget.yaml | 31 + .../wms/complete/expected/service.yaml | 44 + .../wms/complete/input/ownerinfo.yaml | 36 + .../test_data/wms/complete/input/wms.yaml | 308 +++ .../configmap-capabilities-generator.yaml | 150 ++ .../configmap-featureinfo-generator.yaml | 52 + .../expected/configmap-init-scripts.yaml | 189 ++ .../expected/configmap-legend-generator.yaml | 26 + .../expected/configmap-mapserver.yaml | 146 ++ .../configmap-ogc-webservice-proxy.yaml | 31 + .../custom-mapfile/expected/deployment.yaml | 341 ++++ .../expected/horizontalpodautoscaler.yaml | 52 + .../custom-mapfile/expected/ingressroute.yaml | 43 + .../expected/middleware-headers.yaml | 26 + .../expected/poddisruptionbudget.yaml | 29 + .../wms/custom-mapfile/expected/service.yaml | 42 + .../wms/custom-mapfile/input/ownerinfo.yaml | 36 + .../wms/custom-mapfile/input/wms.yaml | 126 ++ .../configmap-capabilities-generator.yaml | 150 ++ .../configmap-featureinfo-generator.yaml | 52 + .../expected/configmap-init-scripts.yaml | 189 ++ .../expected/configmap-legend-generator.yaml | 26 + .../expected/configmap-mapfile-generator.yaml | 118 ++ .../minimal/expected/configmap-mapserver.yaml | 146 ++ .../configmap-ogc-webservice-proxy.yaml | 31 + .../wms/minimal/expected/deployment.yaml | 364 ++++ .../expected/horizontalpodautoscaler.yaml | 52 + .../wms/minimal/expected/ingressroute.yaml | 43 + .../minimal/expected/middleware-headers.yaml | 26 + .../minimal/expected/poddisruptionbudget.yaml | 29 + .../wms/minimal/expected/service.yaml | 42 + .../wms/minimal/input/ownerinfo.yaml | 36 + .../test_data/wms/minimal/input/wms.yaml | 131 ++ .../configmap-capabilities-generator.yaml | 150 ++ .../configmap-featureinfo-generator.yaml | 52 + .../expected/configmap-legend-generator.yaml | 26 + .../expected/configmap-mapfile-generator.yaml | 118 ++ .../expected/configmap-mapserver.yaml | 146 ++ .../configmap-ogc-webservice-proxy.yaml | 31 + .../wms/noprefetch/expected/deployment.yaml | 356 ++++ .../expected/horizontalpodautoscaler.yaml | 52 + .../wms/noprefetch/expected/ingressroute.yaml | 43 + .../expected/middleware-headers.yaml | 26 + .../expected/poddisruptionbudget.yaml | 29 + .../wms/noprefetch/expected/service.yaml | 42 + .../wms/noprefetch/input/ownerinfo.yaml | 36 + .../test_data/wms/noprefetch/input/wms.yaml | 132 ++ .../configmap-capabilities-generator.yaml | 150 ++ .../configmap-featureinfo-generator.yaml | 52 + .../expected/configmap-init-scripts.yaml | 189 ++ .../expected/configmap-legend-generator.yaml | 26 + .../expected/configmap-mapfile-generator.yaml | 118 ++ .../patches/expected/configmap-mapserver.yaml | 146 ++ .../configmap-ogc-webservice-proxy.yaml | 31 + .../wms/patches/expected/deployment.yaml | 372 ++++ .../expected/horizontalpodautoscaler.yaml | 49 + .../wms/patches/expected/ingressroute.yaml | 43 + .../patches/expected/middleware-headers.yaml | 26 + .../patches/expected/poddisruptionbudget.yaml | 29 + .../wms/patches/expected/service.yaml | 42 + .../wms/patches/input/ownerinfo.yaml | 36 + .../test_data/wms/patches/input/wms.yaml | 255 +++ internal/controller/types/types.go | 21 + internal/controller/utils/utils.go | 66 + internal/controller/wfs_controller.go | 118 +- internal/controller/wfs_controller_test.go | 340 +++- internal/controller/wms_controller.go | 195 +- internal/controller/wms_controller_test.go | 298 ++- internal/webhook/v3/shared_webhook.go | 114 ++ internal/webhook/v3/test_data/ownerinfo.yaml | 39 + internal/webhook/v3/test_data/v3_wfs.yaml | 74 + internal/webhook/v3/test_data/v3_wms.yaml | 75 + internal/webhook/v3/webhook_suite_test.go | 122 +- internal/webhook/v3/wfs_webhook.go | 70 +- internal/webhook/v3/wfs_webhook_test.go | 480 ++++- internal/webhook/v3/wms_webhook.go | 96 +- internal/webhook/v3/wms_webhook_test.go | 676 ++++++- test/e2e/e2e_suite_test.go | 36 +- test/e2e/e2e_test.go | 223 +-- test/utils/utils.go | 7 +- 245 files changed, 33933 insertions(+), 1435 deletions(-) create mode 100644 .github/workflows/build-and-publish-image.yml create mode 100644 DEVELOPMENT.md create mode 100644 LICENSE create mode 100644 api/v2beta1/shared_conversion.go create mode 100644 api/v2beta1/shared_types.go create mode 100644 api/v2beta1/wms_conversion_test.go create mode 100644 api/v3/shared_types.go create mode 100644 api/v3/shared_validation.go create mode 100644 api/v3/wfs_validation.go create mode 100644 api/v3/wms_types_test.go create mode 100644 api/v3/wms_validation.go create mode 100755 build-push-deploy-locally.sh create mode 100644 config/crd/bases/embed.go create mode 100644 config/crd/bases/pdok.nl_wfs.yaml create mode 100644 config/crd/bases/pdok.nl_wms.yaml create mode 100644 config/crd/update_openapi.go create mode 100644 config/samples/samples.go create mode 100644 internal/controller/blobdownload/blob_download.go create mode 100644 internal/controller/blobdownload/blob_download_test.go create mode 100644 internal/controller/blobdownload/gpkg_download.sh create mode 100644 internal/controller/capabilitiesgenerator/capabilities_generator.go create mode 100644 internal/controller/capabilitiesgenerator/capabilities_generator_test.go create mode 100644 internal/controller/capabilitiesgenerator/default_bboxes.go create mode 100644 internal/controller/capabilitiesgenerator/mapper.go create mode 100644 internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml create mode 100644 internal/controller/capabilitiesgenerator/test_data/wms_input.yaml create mode 100644 internal/controller/configmaps.go create mode 100644 internal/controller/configmaps_test.go create mode 100644 internal/controller/constants/constants.go create mode 100644 internal/controller/deployment.go create mode 100644 internal/controller/featureinfogenerator/featureinfo_generator.go create mode 100644 internal/controller/featureinfogenerator/featureinfo_generator_test.go create mode 100644 internal/controller/featureinfogenerator/mapper.go create mode 100644 internal/controller/horizontalpodautoscaler.go create mode 100644 internal/controller/ingressroute.go create mode 100755 internal/controller/legendgenerator/legend-fixer.sh create mode 100644 internal/controller/legendgenerator/legend_generator.go create mode 100644 internal/controller/legendgenerator/legend_generator_test.go create mode 100644 internal/controller/legendgenerator/mapper.go create mode 100644 internal/controller/legendgenerator/test_data/expected/legend-fix.yaml create mode 100644 internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml create mode 100644 internal/controller/legendgenerator/test_data/input/legend-fix.yaml create mode 100644 internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml create mode 100644 internal/controller/mapfilegenerator/mapfile_generator.go create mode 100644 internal/controller/mapfilegenerator/mapfile_generator_test.go create mode 100644 internal/controller/mapfilegenerator/mapper.go create mode 100644 internal/controller/mapfilegenerator/test_data/expected/wfs.json create mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_group.json create mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json create mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json create mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json create mode 100644 internal/controller/mapfilegenerator/test_data/expected/wms_tif.json create mode 100644 internal/controller/mapfilegenerator/test_data/input/wfs.yaml create mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_group.yaml create mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml create mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml create mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml create mode 100644 internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml create mode 100644 internal/controller/mapfilegenerator/types.go create mode 100644 internal/controller/mapperutils/utils.go create mode 100644 internal/controller/mapserver/deployment.go create mode 100644 internal/controller/mapserver/deployment_test.go create mode 100644 internal/controller/mapserver/test_data/expected_livenessprobe.yaml create mode 100644 internal/controller/mapserver/test_data/expected_readinessprobe.yaml create mode 100644 internal/controller/mapserver/test_data/expected_startupprobe.yaml create mode 100644 internal/controller/mapserver/test_data/expected_volumemounts.yaml create mode 100644 internal/controller/mapserver/test_data/v2_input.yaml create mode 100644 internal/controller/middleware.go create mode 100644 internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go create mode 100644 internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go create mode 100644 internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml create mode 100644 internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml create mode 100644 internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml create mode 100644 internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml create mode 100644 internal/controller/poddisruptionbudget.go create mode 100644 internal/controller/reconciler.go create mode 100644 internal/controller/service.go create mode 100644 internal/controller/shared_controller.go create mode 100644 internal/controller/shared_controller_test.go create mode 100644 internal/controller/static/files/default_mapserver.conf create mode 100644 internal/controller/static/files/include.conf create mode 100644 internal/controller/static/files/ogc.lua create mode 100644 internal/controller/static/files/scraping-error.xml create mode 100644 internal/controller/static/reader.go create mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/deployment.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wfs/complete/expected/service.yaml create mode 100644 internal/controller/test_data/wfs/complete/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wfs/complete/input/wfs.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/deployment.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wfs/minimal/expected/service.yaml create mode 100644 internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wfs/minimal/input/wfs.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/expected/service.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wfs/noprefetch/input/wfs.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/deployment.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wms/complete/expected/service.yaml create mode 100644 internal/controller/test_data/wms/complete/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wms/complete/input/wms.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/expected/service.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wms/custom-mapfile/input/wms.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/deployment.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wms/minimal/expected/service.yaml create mode 100644 internal/controller/test_data/wms/minimal/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wms/minimal/input/wms.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/deployment.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/expected/service.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wms/noprefetch/input/wms.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/deployment.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/ingressroute.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/middleware-headers.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml create mode 100644 internal/controller/test_data/wms/patches/expected/service.yaml create mode 100644 internal/controller/test_data/wms/patches/input/ownerinfo.yaml create mode 100644 internal/controller/test_data/wms/patches/input/wms.yaml create mode 100644 internal/controller/types/types.go create mode 100644 internal/controller/utils/utils.go create mode 100644 internal/webhook/v3/shared_webhook.go create mode 100644 internal/webhook/v3/test_data/ownerinfo.yaml create mode 100644 internal/webhook/v3/test_data/v3_wfs.yaml create mode 100644 internal/webhook/v3/test_data/v3_wms.yaml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0e0eed2..4dafb3c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Kubebuilder DevContainer", - "image": "docker.io/golang:1.23", + "image": "docker.io/golang:1.24", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {}, "ghcr.io/devcontainers/features/git:1": {} diff --git a/.github/workflows/build-and-publish-image.yml b/.github/workflows/build-and-publish-image.yml new file mode 100644 index 0000000..4f09751 --- /dev/null +++ b/.github/workflows/build-and-publish-image.yml @@ -0,0 +1,67 @@ +--- +name: Build +env: + image: pdok/mapserver-operator +on: + push: + tags: + - '*' +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.image }} + tags: | + type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{version}} + - name: Login to PDOK Docker Hub + if: startsWith(env.image, 'pdok/') + uses: docker/login-action@v1 + with: + username: koalapdok + password: ${{ secrets.DOCKERHUB_PUSH }} + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new + - # Temp fix to cleanup cache + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + - name: Build result notification + if: success() || failure() + uses: 8398a7/action-slack@v3 + with: + fields: all + status: custom + custom_payload: | + { + attachments: [{ + color: '${{ job.status }}' === 'success' ? 'good' : '${{ job.status }}' === 'failure' ? 'danger' : 'warning', + text: `${process.env.AS_WORKFLOW} ${{ job.status }} for ${process.env.AS_REPO}!\n${process.env.AS_JOB} job on ${process.env.AS_REF} (commit: ${process.env.AS_COMMIT}, version: ${{ steps.docker_meta.outputs.version }}) by ${process.env.AS_AUTHOR} took ${process.env.AS_TOOK}`, + }] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4951e33..0c9f2db 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,11 +2,13 @@ name: Lint on: push: + branches: + - master pull_request: jobs: lint: - name: Run on Ubuntu + name: Linting on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code @@ -20,4 +22,4 @@ jobs: - name: Run linter uses: golangci/golangci-lint-action@v6 with: - version: v1.63.4 + version: v1.64.8 diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index b2eda8c..0ce9473 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -2,11 +2,13 @@ name: E2E Tests on: push: + branches: + - master pull_request: jobs: test-e2e: - name: Run on Ubuntu + name: End-2-End on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc2e80d..1117ec9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,11 +2,13 @@ name: Tests on: push: + branches: + - master pull_request: jobs: test: - name: Run on Ubuntu + name: Testing on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code diff --git a/.golangci.yml b/.golangci.yml index 6b29746..331b6f2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,47 +1,116 @@ +--- run: + # Timeout for analysis. timeout: 5m - allow-parallel-runners: true + + # Modules download mode (do not modify go.mod) + modules-download-mode: readonly + + # Include test files (see below to exclude certain linters) + tests: true issues: - # don't skip warning about doc comments - # don't exclude the default set of lint - exclude-use-default: false - # restore some of the defaults - # (fill in the rest as needed) exclude-rules: - - path: "api/*" - linters: - - lll - - path: "internal/*" + # Exclude certain linters for test code + - path: "_test\\.go" linters: + - bodyclose - dupl - - lll -linters: - disable-all: true - enable: - - dupl - - errcheck - - copyloopvar - - ginkgolinter - - goconst - - gocyclo - - gofmt - - goimports - - gosimple - - govet - - ineffassign - - lll - - misspell - - nakedret - - prealloc - - revive - - staticcheck - - typecheck - - unconvert - - unparam - - unused + - dogsled + - funlen + - gosec + +output: + formats: + - format: colored-line-number + path: stdout + print-issued-lines: true + print-linter-name: true linters-settings: - revive: + depguard: rules: - - name: comment-spacings + main: + # Packages that are not allowed where the value is a suggestion. + deny: + - pkg: "github.com/pkg/errors" + desc: Should be replaced by standard lib errors package + cyclop: + # The maximal code complexity to report. + max-complexity: 15 + skip-tests: true + funlen: + lines: 100 + nestif: + min-complexity: 6 + forbidigo: + forbid: + - http\.NotFound.* # return RFC 7807 problem details instead + - http\.Error.* # return RFC 7807 problem details instead + gomoddirectives: + replace-allow-list: + - github.com/abbot/go-http-auth # https://github.com/traefik/traefik/issues/6873#issuecomment-637654361 + +linters: + disable-all: true + enable: + # enabled by default by golangci-lint + - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases + - gosimple # specializes in simplifying a code + - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # detects when assignments to existing variables are not used + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks + - typecheck # like the front-end of a Go compiler, parses and type-checks Go code + - unused # checks for unused constants, variables, functions and types + # extra enabled by us + - asasalint # checks for pass []any as any in variadic func(...any) + - asciicheck # checks that your code does not contain non-ASCII identifiers + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - cyclop # checks function and package cyclomatic complexity + - dupl # tool for code clone detection + - durationcheck # checks for two durations multiplied together + - dogsled # find assignments/declarations with too many blank identifiers + - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error + - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 + - exhaustive # checks exhaustiveness of enum switch statements + - exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions + - copyloopvar # checks for pointers to enclosing loop variables + - fatcontext # detects nested contexts in loops and function literals + - forbidigo # forbids identifiers + - funlen # tool for detection of long functions + - gocheckcompilerdirectives # validates go compiler directive comments (//go:) + - goconst # finds repeated strings that could be replaced by a constant + - gocritic # provides diagnostics that check for bugs, performance and style issues + - gofmt # checks if the code is formatted according to 'gofmt' command + - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt + - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod + - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects source code for security problems + - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) + - makezero # finds slice declarations with non-zero initial length + - mirror # reports wrong mirror patterns of bytes/strings usage + - misspell # finds commonly misspelled English words + - nakedret # finds naked returns in functions greater than a specified function length + - nestif # reports deeply nested if statements + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - nolintlint # reports ill-formed or insufficient nolint directives + - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL + - perfsprint # Golang linter for performance, aiming at usages of fmt.Sprintf which have faster alternatives + - predeclared # finds code that shadows one of Go's predeclared identifiers + - promlinter # checks Prometheus metrics naming via promlint + - reassign # checks that package variables are not reassigned + - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - sloglint # A Go linter that ensures consistent code style when using log/slog + - tagliatelle # checks the struct tags. + - testableexamples # checks if examples are testable (have an expected output) + - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes + - usetesting # detects using os.Setenv instead of t.Setenv since Go1.17 + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - usestdlibvars # detects the possibility to use variables/constants from the Go standard library + - wastedassign # finds wasted assignment statements + fast: false \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..ff22131 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,5 @@ +## Local testing + +- Start an empty cluster using `k8s-clusters/local-test/empty-cluster.sh` +- Build and push the controller to the cluster using `build-and-push-locally.sh ` +- Deploy a service to the cluster, for example (running from `k8s-clusters/local-test`): `OWNER=kadaster TECHNICAL_NAME=ad docker-compose -f ./docker-compose.yaml -f ./bundle-pollers/docker-compose.services.yaml up kustomize-init` diff --git a/Dockerfile b/Dockerfile index 348b837..735a9dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,16 @@ # Build the manager binary -FROM docker.io/golang:1.23 AS builder +FROM docker.io/golang:1.24 AS builder ARG TARGETOS ARG TARGETARCH +#COPY --from=repos ./smooth-operator /smooth-operator +#COPY --from=repos ./ogc-specifications /ogc-specifications + WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum + # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1e6423f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-2025 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index 9a4ab3b..bdc20dd 100644 --- a/Makefile +++ b/Makefile @@ -43,7 +43,9 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) rbac:roleName=manager-role crd:allowDangerousTypes=true webhook paths="./..." output:crd:artifacts:config=config/crd/bases + go run config/crd/update_openapi.go config/crd/bases +## allowDangerousTypes=true for v2beta structs .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. @@ -63,7 +65,8 @@ test: manifests generate fmt vet setup-envtest ## Run tests. # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. -# CertManager is installed by default; skip with: +# Prometheus and CertManager are installed by default; skip with: +# - PROMETHEUS_INSTALL_SKIP=true # - CERT_MANAGER_INSTALL_SKIP=true .PHONY: test-e2e test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. @@ -172,12 +175,12 @@ GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions KUSTOMIZE_VERSION ?= v5.5.0 -CONTROLLER_TOOLS_VERSION ?= v0.17.2 +CONTROLLER_TOOLS_VERSION ?= v0.17.1 #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v1.63.4 +GOLANGCI_LINT_VERSION ?= v1.64.8 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. diff --git a/README.md b/README.md index e75fcab..afabaa5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,20 @@ # mapserver-operator -// TODO(user): Add simple overview of use/purpose +_Kubernetes controller/operator to serve WFS and WMS instances._ + +[![Build](https://github.com/PDOK/mapserver-operator/actions/workflows/build-and-publish-image.yml/badge.svg)](https://github.com/PDOK/mapserver-operator/actions/workflows/build-and-publish-image.yml) +[![Lint (go)](https://github.com/PDOK/mapserver-operator/actions/workflows/lint.yml/badge.svg)](https://github.com/PDOK/mapserver-operator/actions/workflows/lint.yml) +[![GitHub license](https://img.shields.io/github/license/PDOK/mapserver-operator)](https://github.com/PDOK/mapserver-operator/blob/master/LICENSE) ## Description -// TODO(user): An in-depth paragraph about your project and overview of use +This Kubernetes controller cq operator (an operator could be described as a specialized controller) +ensures that the necessary resources are created or kept up-to-date in a cluster +to deploy instances of the [Web Map Service](https://www.ogc.org/standards/wms/)(WMS) and [Web Features Service](https://www.ogc.org/standards/wfs/)(WFS). This repository is a complete solution to deploy WMS and WFS services according to CR schemas. +This operator uses two Custom Resources(CR) called _WMS_ and _WFS_ as the input for the deployment, which is also defined in this repository. ## Getting Started ### Prerequisites -- go version v1.23.0+ +- go version v1.24.0+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. @@ -66,70 +73,54 @@ make uninstall make undeploy ``` -## Project Distribution +## Develop -Following the options to release and provide this solution to the users. +The project is written in Go and scaffolded with [kubebuilder](https://kubebuilder.io). -### By providing a bundle with all YAML files +### kubebuilder -1. Build the installer for the image built and published in the registry: +Read the manual when you want/need to make changes. +E.g. run `make test` before committing. -```sh -make build-installer IMG=/mapserver-operator:tag -``` +### Linting -**NOTE:** The makefile target mentioned above generates an 'install.yaml' -file in the dist directory. This file contains all the resources built -with Kustomize, which are necessary to install this project without its -dependencies. +Install [golangci-lint](https://golangci-lint.run/usage/install/) and run `golangci-lint run` +from the root. +(Don't run `make lint`, it uses an old version of golangci-lint.) -2. Using the installer +# Contributing -Users can just run 'kubectl apply -f ' to install -the project, i.e.: +### How to contribute +Mapserver-operator is solely developed by PDOK. Contributions are however always welcome. If you have any questions or suggestions you can create an issue in the issue tracker. -```sh -kubectl apply -f https://raw.githubusercontent.com//mapserver-operator//dist/install.yaml -``` +### Contact +The maintainers can be contacted through the issue tracker. -### By providing a Helm Chart +# Authors +This project is developed by [PDOK](https://www.pdok.nl/), a platform for publication of geographic datasets of Dutch governmental institutions. -1. Build the chart using the optional helm plugin +# License -```sh -kubebuilder edit --plugins=helm/v1-alpha ``` - -2. See that a chart was generated under 'dist/chart', and users -can obtain this solution from there. - -**NOTE:** If you change the project, you need to update the Helm Chart -using the same command above to sync the latest changes. Furthermore, -if you create webhooks, you need to use the above command with -the '--force' flag and manually ensure that any custom configuration -previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' -is manually re-applied afterwards. - -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project - -**NOTE:** Run `make help` for more information on all potential `make` targets - -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) - -## License - -Copyright 2025. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - +MIT License + +Copyright (c) 2024-2025 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/api/v2beta1/groupversion_info.go b/api/v2beta1/groupversion_info.go index 0c3c988..7033d98 100644 --- a/api/v2beta1/groupversion_info.go +++ b/api/v2beta1/groupversion_info.go @@ -1,17 +1,25 @@ /* -Copyright 2025. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ // Package v2beta1 contains API Schema definitions for the v2beta1 API group. diff --git a/api/v2beta1/shared_conversion.go b/api/v2beta1/shared_conversion.go new file mode 100644 index 0000000..f497eb6 --- /dev/null +++ b/api/v2beta1/shared_conversion.go @@ -0,0 +1,289 @@ +package v2beta1 + +import ( + "net/url" + "strings" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/constants" + smoothoperatormodel "github.com/pdok/smooth-operator/model" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" +) + +func fixUnicode(val string) string { + return strings.ReplaceAll(val, "\\xF6", "รถ") +} + +func ConvertOptionsV2ToV3(src *WMSWFSOptions) *pdoknlv3.Options { + defaults := pdoknlv3.GetDefaultOptions() + + if src == nil { + return defaults + } + + return &pdoknlv3.Options{ + BaseOptions: pdoknlv3.BaseOptions{ + AutomaticCasing: src.AutomaticCasing, + IncludeIngress: src.IncludeIngress, + PrefetchData: smoothoperatorutils.PointerVal(src.PrefetchData, defaults.PrefetchData), + }, + WMSOptions: pdoknlv3.WMSOptions{ + ValidateRequests: smoothoperatorutils.PointerVal(src.ValidateRequests, defaults.ValidateRequests), + RewriteGroupToDataLayers: smoothoperatorutils.PointerVal(src.RewriteGroupToDataLayers, defaults.RewriteGroupToDataLayers), + DisableWebserviceProxy: smoothoperatorutils.PointerVal(src.DisableWebserviceProxy, defaults.DisableWebserviceProxy), + ValidateChildStyleNameEqual: smoothoperatorutils.PointerVal(src.ValidateChildStyleNameEqual, defaults.ValidateChildStyleNameEqual), + }, + } +} + +func ConvertOptionsV3ToV2(src *pdoknlv3.Options) *WMSWFSOptions { + if src == nil { + src = pdoknlv3.GetDefaultOptions() + } + + return &WMSWFSOptions{ + AutomaticCasing: src.AutomaticCasing, + IncludeIngress: src.IncludeIngress, + PrefetchData: &src.PrefetchData, + ValidateRequests: &src.ValidateRequests, + RewriteGroupToDataLayers: &src.RewriteGroupToDataLayers, + DisableWebserviceProxy: &src.DisableWebserviceProxy, + ValidateChildStyleNameEqual: &src.ValidateChildStyleNameEqual, + } +} + +//nolint:gosec +func ConvertAutoscaling(src Autoscaling) *pdoknlv3.HorizontalPodAutoscalerPatch { + hpa := &pdoknlv3.HorizontalPodAutoscalerPatch{} + + if src.MinReplicas != nil { + //nolint:gosec + hpa.MinReplicas = smoothoperatorutils.Pointer(int32(*src.MinReplicas)) + } + + if src.MaxReplicas != nil { + //nolint:gosec + hpa.MaxReplicas = smoothoperatorutils.Pointer(int32(*src.MaxReplicas)) + } + + metrics := make([]autoscalingv2.MetricSpec, 0) + if src.AverageCPUUtilization != nil { + metrics = append(metrics, autoscalingv2.MetricSpec{ + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: smoothoperatorutils.Pointer(int32(*src.AverageCPUUtilization)), + }, + }, + }) + hpa.Metrics = metrics + } + + return hpa +} + +func ConvertResources(src corev1.ResourceRequirements) corev1.PodSpec { + targetResources := src + + if src.Requests != nil { + if val, ok := src.Requests["ephemeralStorage"]; ok { + targetResources.Requests[corev1.ResourceEphemeralStorage] = val + delete(targetResources.Requests, "ephemeralStorage") + } + } + if src.Limits != nil { + if val, ok := src.Limits["ephemeralStorage"]; ok { + targetResources.Limits[corev1.ResourceEphemeralStorage] = val + delete(targetResources.Limits, "ephemeralStorage") + } + } + + return corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: constants.MapserverName, + Resources: targetResources, + }, + }, + } +} + +func ConvertColumnAndAliasesV2ToColumnsWithAliasV3(columns []string, aliases map[string]string) []pdoknlv3.Column { + v3Columns := make([]pdoknlv3.Column, 0) + for _, column := range columns { + col := pdoknlv3.Column{ + Name: column, + } + + // TODO - multiple aliases per column possible? + if alias, ok := aliases[column]; ok { + col.Alias = &alias + } + + v3Columns = append(v3Columns, col) + } + + return v3Columns +} + +func ConvertColumnsWithAliasV3ToColumnsAndAliasesV2(columns []pdoknlv3.Column) ([]string, map[string]string) { + v2Columns := make([]string, 0) + v2Aliases := make(map[string]string) + + for _, col := range columns { + v2Columns = append(v2Columns, col.Name) + + if col.Alias != nil { + v2Aliases[col.Name] = *col.Alias + } + } + + return v2Columns, v2Aliases +} + +func ConvertV2DataToV3(v2 Data) pdoknlv3.Data { + v3 := pdoknlv3.Data{} + + if v2.GPKG != nil { + v3.Gpkg = &pdoknlv3.Gpkg{ + BlobKey: v2.GPKG.BlobKey, + TableName: v2.GPKG.Table, + GeometryType: v2.GPKG.GeometryType, + Columns: ConvertColumnAndAliasesV2ToColumnsWithAliasV3( + v2.GPKG.Columns, + v2.GPKG.Aliases, + ), + } + } + + if v2.Postgis != nil { + v3.Postgis = &pdoknlv3.Postgis{ + TableName: v2.Postgis.Table, + GeometryType: v2.Postgis.GeometryType, + Columns: ConvertColumnAndAliasesV2ToColumnsWithAliasV3( + v2.Postgis.Columns, + v2.Postgis.Aliases, + ), + } + } + + if v2.Tif != nil { + v3.TIF = &pdoknlv3.TIF{ + BlobKey: v2.Tif.BlobKey, + Resample: smoothoperatorutils.PointerVal(v2.Tif.Resample, "NEAREST"), + Offsite: v2.Tif.Offsite, + GetFeatureInfoIncludesClass: smoothoperatorutils.PointerVal(v2.Tif.GetFeatureInfoIncludesClass, false), + } + } + + return v3 +} + +func ConvertV3DataToV2(v3 pdoknlv3.Data) Data { + v2 := Data{} + + if v3.Gpkg != nil { + columns, aliases := ConvertColumnsWithAliasV3ToColumnsAndAliasesV2(v3.Gpkg.Columns) + v2.GPKG = &GPKG{ + BlobKey: v3.Gpkg.BlobKey, + Table: v3.Gpkg.TableName, + GeometryType: v3.Gpkg.GeometryType, + Columns: columns, + Aliases: aliases, + } + } + + if v3.Postgis != nil { + columns, aliases := ConvertColumnsWithAliasV3ToColumnsAndAliasesV2(v3.Postgis.Columns) + v2.Postgis = &Postgis{ + Table: v3.Postgis.TableName, + GeometryType: v3.Postgis.GeometryType, + Columns: columns, + Aliases: aliases, + } + } + + if v3.TIF != nil { + v2.Tif = &Tif{ + BlobKey: v3.TIF.BlobKey, + Offsite: v3.TIF.Offsite, + Resample: &v3.TIF.Resample, + GetFeatureInfoIncludesClass: &v3.TIF.GetFeatureInfoIncludesClass, + } + } + + return v2 +} + +func NewV2KubernetesObject(lifecycle *smoothoperatormodel.Lifecycle, podSpecPatch corev1.PodSpec, scalingSpec *pdoknlv3.HorizontalPodAutoscalerPatch) Kubernetes { + kub := Kubernetes{} + + if lifecycle != nil && lifecycle.TTLInDays != nil { + kub.Lifecycle = &Lifecycle{ + TTLInDays: smoothoperatorutils.Pointer(int(*lifecycle.TTLInDays)), + } + } + + kub.Resources = &podSpecPatch.Containers[0].Resources + + if scalingSpec != nil { + kub.Autoscaling = &Autoscaling{} + + if scalingSpec.MaxReplicas != nil { + kub.Autoscaling.MaxReplicas = smoothoperatorutils.Pointer(int(*scalingSpec.MaxReplicas)) + } + + if scalingSpec.MinReplicas != nil { + kub.Autoscaling.MinReplicas = smoothoperatorutils.Pointer(int(*scalingSpec.MinReplicas)) + } + + if scalingSpec.Metrics != nil { + kub.Autoscaling.AverageCPUUtilization = smoothoperatorutils.Pointer( + int(*scalingSpec.Metrics[0].Resource.Target.AverageUtilization), + ) + } + } + + return kub +} + +func LabelsToV2General(labels map[string]string) General { + general := General{ + Dataset: labels["dataset"], + DatasetOwner: labels["dataset-owner"], + DataVersion: nil, + } + + if serviceVersion, ok := labels["service-version"]; ok { + general.ServiceVersion = &serviceVersion + } + + if theme, ok := labels["theme"]; ok { + general.Theme = &theme + } + + return general +} + +func CreateBaseURL(host string, kind string, general General) (*smoothoperatormodel.URL, error) { + baseURL, err := url.Parse(host + "/") + if err != nil { + return nil, err + } + baseURL = baseURL.JoinPath(general.DatasetOwner, general.Dataset) + if general.Theme != nil { + baseURL = baseURL.JoinPath(*general.Theme) + } + baseURL = baseURL.JoinPath(kind) + + if general.ServiceVersion != nil { + baseURL = baseURL.JoinPath(*general.ServiceVersion) + } + + return &smoothoperatormodel.URL{URL: baseURL}, nil +} diff --git a/api/v2beta1/shared_types.go b/api/v2beta1/shared_types.go new file mode 100644 index 0000000..9ea921f --- /dev/null +++ b/api/v2beta1/shared_types.go @@ -0,0 +1,145 @@ +package v2beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Status - The status for custom resources managed by the operator-sdk. +type Status struct { + Conditions []Condition `json:"conditions,omitempty"` + Deployment *string `json:"deployment,omitempty"` + Resources []Resources `json:"resources,omitempty"` +} + +// Condition - the condition for the ansible operator +// https://github.com/operator-framework/operator-sdk/blob/master/internal/ansible/controller/status/types.go#L101 +type Condition struct { + Type ConditionType `json:"type"` + Status ConditionStatus `json:"status"` + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + AnsibleResult *ResultAnsible `json:"ansibleResult,omitempty"` + Reason string `json:"reason"` + Message string `json:"message"` +} + +// ConditionType specifies a string for field ConditionType +type ConditionType string + +// ConditionStatus specifies a string for field ConditionType +type ConditionStatus string + +// This const specifies allowed fields for Status +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// ResultAnsible - encapsulation of the ansible result. 'AnsibleResult' is turned around in struct to comply with linting +type ResultAnsible struct { + Ok int `json:"ok"` + Changed int `json:"changed"` + Skipped int `json:"skipped"` + Failures int `json:"failures"` + TimeOfCompletion string `json:"completion"` +} + +// Resources is the struct for the resources field within status +type Resources struct { + APIVersion *string `json:"apiversion,omitempty"` + Kind *string `json:"kind,omitempty"` + Name *string `json:"name,omitempty"` +} + +// General is the struct with all generic fields for the crds +type General struct { + Dataset string `json:"dataset"` + Theme *string `json:"theme,omitempty"` + DatasetOwner string `json:"datasetOwner"` + ServiceVersion *string `json:"serviceVersion,omitempty"` + DataVersion *string `json:"dataVersion,omitempty"` +} + +// Kubernetes is the struct with all fields that can be defined in kubernetes fields in the crds +type Kubernetes struct { + Autoscaling *Autoscaling `json:"autoscaling,omitempty"` + HealthCheck *HealthCheck `json:"healthCheck,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + Lifecycle *Lifecycle `json:"lifecycle,omitempty"` +} + +// Autoscaling is the struct with all fields to configure autoscalers for the crs +type Autoscaling struct { + AverageCPUUtilization *int `json:"averageCpuUtilization,omitempty"` + MinReplicas *int `json:"minReplicas,omitempty"` + MaxReplicas *int `json:"maxReplicas,omitempty"` +} + +// HealthCheck is the struct with all fields to configure healthchecks for the crs +type HealthCheck struct { + Querystring *string `json:"querystring,omitempty"` + Mimetype *string `json:"mimetype,omitempty"` + Boundingbox *string `json:"boundingbox,omitempty"` +} + +// Lifecycle is the struct with the fields to configure lifecycle settings for the resources +type Lifecycle struct { + TTLInDays *int `json:"ttlInDays,omitempty"` +} + +// WMSWFSOptions is the struct with options available in the operator +type WMSWFSOptions struct { + // +kubebuilder:default:=true + IncludeIngress bool `json:"includeIngress"` + // +kubebuilder:default:=true + AutomaticCasing bool `json:"automaticCasing"` + // +kubebuilder:default:=true + ValidateRequests *bool `json:"validateRequests,omitempty"` + RewriteGroupToDataLayers *bool `json:"rewriteGroupToDataLayers,omitempty"` + DisableWebserviceProxy *bool `json:"disableWebserviceProxy,omitempty"` + // +kubebuilder:default:=true + PrefetchData *bool `json:"prefetchData,omitempty"` + ValidateChildStyleNameEqual *bool `json:"validateChildStyleNameEqual,omitempty"` +} + +// Authority is a struct for the authority fields in WMS and WFS crds +type Authority struct { + Name string `json:"name"` + URL string `json:"url"` +} + +// Data is a struct for the data field for a WMSLayer or WFS FeatureType +type Data struct { + GPKG *GPKG `json:"gpkg,omitempty"` + Postgis *Postgis `json:"postgis,omitempty"` + Tif *Tif `json:"tif,omitempty"` +} + +// GPKG is a struct for the gpkg field for a WMSLayer or WFS FeatureType +type GPKG struct { + BlobKey string `json:"blobKey"` + Table string `json:"table"` + GeometryType string `json:"geometryType"` + Columns []string `json:"columns"` + // In a new version Aliases should become part of Columns + Aliases map[string]string `json:"aliases,omitempty"` +} + +// Postgis is a struct for the Postgis db config for a WMSLayer or WFS FeatureType +// connection details are passed through the environment +type Postgis struct { + Table string `json:"table"` + GeometryType string `json:"geometryType"` + Columns []string `json:"columns"` + // In a new version Aliases should become part of Columns + Aliases map[string]string `json:"aliases,omitempty"` +} + +// Tif is a struct for the Tif field for a WMSLayer +type Tif struct { + BlobKey string `json:"blobKey"` + GetFeatureInfoIncludesClass *bool `json:"getFeatureInfoIncludesClass,omitempty"` + Offsite *string `json:"offsite,omitempty"` + Resample *string `json:"resample,omitempty"` +} diff --git a/api/v2beta1/wfs_conversion.go b/api/v2beta1/wfs_conversion.go index 54a3687..24664fc 100644 --- a/api/v2beta1/wfs_conversion.go +++ b/api/v2beta1/wfs_conversion.go @@ -1,27 +1,38 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v2beta1 import ( "log" - - "sigs.k8s.io/controller-runtime/pkg/conversion" + "strconv" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatormodel "github.com/pdok/smooth-operator/model" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + + "sigs.k8s.io/controller-runtime/pkg/conversion" ) // ConvertTo converts this WFS (v2beta1) to the Hub version (v3). @@ -30,16 +41,246 @@ func (src *WFS) ConvertTo(dstRaw conversion.Hub) error { log.Printf("ConvertTo: Converting WFS from Spoke version v2beta1 to Hub version v3;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) - // TODO(user): Implement conversion logic from v2beta1 to v3 + return src.ToV3(dst) +} + +//nolint:gosec,funlen,cyclop +func (src *WFS) ToV3(dst *pdoknlv3.WFS) error { + dst.ObjectMeta = src.ObjectMeta + + // Set LifeCycle if defined + if src.Spec.Kubernetes.Lifecycle != nil && src.Spec.Kubernetes.Lifecycle.TTLInDays != nil { + dst.Spec.Lifecycle = &smoothoperatormodel.Lifecycle{ + TTLInDays: smoothoperatorutils.Pointer(int32(*src.Spec.Kubernetes.Lifecycle.TTLInDays)), + } + } + + if src.Spec.Kubernetes.Autoscaling != nil { + dst.Spec.HorizontalPodAutoscalerPatch = ConvertAutoscaling(*src.Spec.Kubernetes.Autoscaling) + } + + if src.Spec.Kubernetes.Resources != nil { + dst.Spec.PodSpecPatch = ConvertResources(*src.Spec.Kubernetes.Resources) + } + + dst.Spec.Options = &ConvertOptionsV2ToV3(src.Spec.Options).BaseOptions + + if src.Spec.Kubernetes.HealthCheck != nil { + dst.Spec.HealthCheck = &pdoknlv3.HealthCheckWFS{ + Querystring: *src.Spec.Kubernetes.HealthCheck.Querystring, + Mimetype: *src.Spec.Kubernetes.HealthCheck.Mimetype, + } + } + + url, err := CreateBaseURL(pdoknlv3.GetHost(true), "wfs", src.Spec.General) + if err != nil { + return err + } + + accessConstraints, err := url.Parse("https://creativecommons.org/publicdomain/zero/1.0/deed.nl") + if err != nil { + return err + } + if src.Spec.Service.AccessConstraints != nil { + accessConstraints, err = url.Parse(*src.Spec.Service.AccessConstraints) + if err != nil { + return err + } + } + + allOtherCrs := []string{ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + } + otherCrs := []string{} + for _, cr := range allOtherCrs { + if cr != src.Spec.Service.DataEPSG { + otherCrs = append(otherCrs, cr) + } + } + + service := pdoknlv3.WFSService{ + BaseService: pdoknlv3.BaseService{ + Prefix: src.Spec.General.Dataset, + URL: *url, + OwnerInfoRef: "pdok", + Title: src.Spec.Service.Title, + Abstract: src.Spec.Service.Abstract, + Keywords: src.Spec.Service.Keywords, + Fees: nil, + AccessConstraints: smoothoperatormodel.URL{URL: accessConstraints}, + }, + DefaultCrs: src.Spec.Service.DataEPSG, + OtherCrs: otherCrs, + FeatureTypes: make([]pdoknlv3.FeatureType, 0), + } + + if src.Spec.Service.Maxfeatures != nil { + maxFeatures, err := strconv.Atoi(*src.Spec.Service.Maxfeatures) + if err != nil { + return err + } + service.CountDefault = &maxFeatures + } + + if src.Spec.Service.Mapfile != nil { + service.Mapfile = &pdoknlv3.Mapfile{ + ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, + } + } + + if src.Spec.Service.Extent != nil && *src.Spec.Service.Extent != "" { + service.Bbox = &pdoknlv3.Bbox{ + DefaultCRS: smoothoperatormodel.ExtentToBBox(*src.Spec.Service.Extent), + } + } else { + service.Bbox = &pdoknlv3.Bbox{ + DefaultCRS: smoothoperatormodel.BBox{ + MinX: "-25000", + MaxX: "280000", + MinY: "250000", + MaxY: "860000", + }, + } + } + + // TODO - where to place the MetadataIdentifier and FeatureTypes[0].SourceMetadataIdentifier if the service is not inspire? + if src.Spec.Service.Inspire { + service.Inspire = &pdoknlv3.WFSInspire{Inspire: pdoknlv3.Inspire{ + ServiceMetadataURL: pdoknlv3.MetadataURL{ + CSW: &pdoknlv3.Metadata{ + MetadataIdentifier: src.Spec.Service.MetadataIdentifier, + }, + }, + Language: "dut"}, + SpatialDatasetIdentifier: src.Spec.Service.FeatureTypes[0].SourceMetadataIdentifier, + } + } + + for _, featureType := range src.Spec.Service.FeatureTypes { + service.FeatureTypes = append(service.FeatureTypes, convertV2FeatureTypeToV3(featureType)) + } + + dst.Spec.Service = service + return nil } +func convertV2FeatureTypeToV3(src FeatureType) pdoknlv3.FeatureType { + featureTypeV3 := pdoknlv3.FeatureType{ + Name: src.Name, + Title: src.Title, + Abstract: src.Abstract, + Keywords: src.Keywords, + DatasetMetadataURL: &pdoknlv3.MetadataURL{ + CSW: &pdoknlv3.Metadata{ + MetadataIdentifier: src.DatasetMetadataIdentifier, + }, + }, + Data: pdoknlv3.BaseData{}, + } + + if src.Extent != nil { + featureTypeV3.Bbox = &pdoknlv3.FeatureBbox{ + DefaultCRS: smoothoperatorutils.Pointer(smoothoperatormodel.ExtentToBBox(*src.Extent)), + } + } + + featureTypeV3.Data = ConvertV2DataToV3(src.Data).BaseData + + return featureTypeV3 +} + // ConvertFrom converts the Hub version (v3) to this WFS (v2beta1). +// +//nolint:revive func (dst *WFS) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*pdoknlv3.WFS) log.Printf("ConvertFrom: Converting WFS from Hub version v3 to Spoke version v2beta1;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) - // TODO(user): Implement conversion logic from v3 to v2beta1 + dst.ObjectMeta = src.ObjectMeta + + dst.Spec.General = LabelsToV2General(src.ObjectMeta.Labels) + + dst.Spec.Kubernetes = NewV2KubernetesObject(src.Spec.Lifecycle, src.Spec.PodSpecPatch, src.Spec.HorizontalPodAutoscalerPatch) + + dst.Spec.Options = ConvertOptionsV3ToV2(&pdoknlv3.Options{BaseOptions: *src.Spec.Options}) + + if src.Spec.HealthCheck != nil { + dst.Spec.Kubernetes.HealthCheck = &HealthCheck{ + Querystring: &src.Spec.HealthCheck.Querystring, + Mimetype: &src.Spec.HealthCheck.Mimetype, + } + } + + accessConstraints := src.Spec.Service.AccessConstraints.String() + + service := WFSService{ + Title: src.Spec.Service.Title, + Abstract: src.Spec.Service.Abstract, + Keywords: src.Spec.Service.Keywords, + AccessConstraints: &accessConstraints, + DataEPSG: src.Spec.Service.DefaultCrs, + Authority: Authority{ + Name: "", + URL: "", + }, + } + + if src.Spec.Service.CountDefault != nil { + service.Maxfeatures = smoothoperatorutils.Pointer(strconv.Itoa(*src.Spec.Service.CountDefault)) + } + + if src.Spec.Service.Bbox != nil { + service.Extent = smoothoperatorutils.Pointer(src.Spec.Service.Bbox.DefaultCRS.ToExtent()) + } else { + service.Extent = smoothoperatorutils.Pointer("-25000 250000 280000 860000") + } + + if src.Spec.Service.Mapfile != nil { + service.Mapfile = &Mapfile{ + ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, + } + } + + if src.Spec.Service.Inspire != nil { + service.Inspire = true + service.MetadataIdentifier = src.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier + } else { + service.Inspire = false + // TODO unable to fill in MetadataIdentifier here until we know how to handle non inspire services + } + + for _, featureType := range src.Spec.Service.FeatureTypes { + featureTypeV2 := FeatureType{ + Name: featureType.Name, + Title: featureType.Title, + Abstract: featureType.Abstract, + Keywords: featureType.Keywords, + DatasetMetadataIdentifier: featureType.DatasetMetadataURL.CSW.MetadataIdentifier, + SourceMetadataIdentifier: "", + Data: ConvertV3DataToV2(pdoknlv3.Data{BaseData: featureType.Data}), + } + + if src.Spec.Service.Inspire != nil { + featureTypeV2.SourceMetadataIdentifier = src.Spec.Service.Inspire.SpatialDatasetIdentifier + } + + if featureType.Bbox != nil { + featureTypeV2.Extent = smoothoperatorutils.Pointer(featureType.Bbox.DefaultCRS.ToExtent()) + } + + service.FeatureTypes = append(service.FeatureTypes, featureTypeV2) + } + + dst.Spec.Service = service + return nil } diff --git a/api/v2beta1/wfs_types.go b/api/v2beta1/wfs_types.go index c2dfae2..d349487 100644 --- a/api/v2beta1/wfs_types.go +++ b/api/v2beta1/wfs_types.go @@ -1,17 +1,25 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v2beta1 @@ -23,31 +31,16 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -// WFSSpec defines the desired state of WFS. -type WFSSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of WFS. Edit wfs_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// WFSStatus defines the observed state of WFS. -type WFSStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - // +kubebuilder:object:root=true -// +kubebuilder:subresource:status +// +kubebuilder:skipversion // WFS is the Schema for the wfs API. type WFS struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec WFSSpec `json:"spec,omitempty"` - Status WFSStatus `json:"status,omitempty"` + Spec WFSSpec `json:"spec,omitempty"` + Status *Status `json:"status,omitempty"` } // +kubebuilder:object:root=true @@ -59,6 +52,44 @@ type WFSList struct { Items []WFS `json:"items"` } +// WFSSpec is the struct for all fields defined in the WFS CRD +type WFSSpec struct { + General General `json:"general"` + Service WFSService `json:"service"` + Kubernetes Kubernetes `json:"kubernetes"` + Options *WMSWFSOptions `json:"options,omitempty"` +} + +// WFSService is the struct with all service specific options +type WFSService struct { + Title string `json:"title"` + Inspire bool `json:"inspire"` + Abstract string `json:"abstract"` + // +kubebuilder:default="https://creativecommons.org/publicdomain/zero/1.0/deed.nl" + AccessConstraints *string `json:"accessConstraints,omitempty"` + Keywords []string `json:"keywords"` + MetadataIdentifier string `json:"metadataIdentifier"` + Authority Authority `json:"authority"` + Extent *string `json:"extent,omitempty"` + Maxfeatures *string `json:"maxfeatures,omitempty"` + //nolint:tagliatelle + DataEPSG string `json:"dataEPSG"` + FeatureTypes []FeatureType `json:"featureTypes"` + Mapfile *Mapfile `json:"mapfile,omitempty"` +} + +// FeatureType is the struct for all feature type level fields +type FeatureType struct { + Name string `json:"name"` + Title string `json:"title"` + Abstract string `json:"abstract"` + Keywords []string `json:"keywords"` + DatasetMetadataIdentifier string `json:"datasetMetadataIdentifier"` + SourceMetadataIdentifier string `json:"sourceMetadataIdentifier"` + Extent *string `json:"extent,omitempty"` + Data Data `json:"data"` +} + func init() { SchemeBuilder.Register(&WFS{}, &WFSList{}) } diff --git a/api/v2beta1/wms_conversion.go b/api/v2beta1/wms_conversion.go index 759c837..9c8f121 100644 --- a/api/v2beta1/wms_conversion.go +++ b/api/v2beta1/wms_conversion.go @@ -1,45 +1,692 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v2beta1 import ( + "errors" "log" + "slices" + "strconv" + "strings" - "sigs.k8s.io/controller-runtime/pkg/conversion" + "k8s.io/utils/ptr" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatormodel "github.com/pdok/smooth-operator/model" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + + "sigs.k8s.io/controller-runtime/pkg/conversion" ) +const ServiceMetatdataIdentifierAnnotation = "pdok.nl/wms-service-metadata-uuid" + // ConvertTo converts this WMS (v2beta1) to the Hub version (v3). func (src *WMS) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*pdoknlv3.WMS) log.Printf("ConvertTo: Converting WMS from Spoke version v2beta1 to Hub version v3;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) + return src.ToV3(dst) +} + +//nolint:gosec,cyclop,funlen +func (src *WMS) ToV3(target *pdoknlv3.WMS) error { + dst := target + + dst.ObjectMeta = src.ObjectMeta + if dst.Annotations == nil { + dst.Annotations = make(map[string]string) + } + + // Set LifeCycle if defined + if src.Spec.Kubernetes.Lifecycle != nil && src.Spec.Kubernetes.Lifecycle.TTLInDays != nil { + dst.Spec.Lifecycle = &smoothoperatormodel.Lifecycle{ + TTLInDays: smoothoperatorutils.Pointer(int32(*src.Spec.Kubernetes.Lifecycle.TTLInDays)), + } + } + + if src.Spec.Kubernetes.Autoscaling != nil { + dst.Spec.HorizontalPodAutoscalerPatch = ConvertAutoscaling(*src.Spec.Kubernetes.Autoscaling) + } + + if src.Spec.Kubernetes.Resources != nil { + dst.Spec.PodSpecPatch = ConvertResources(*src.Spec.Kubernetes.Resources) + } + + dst.Spec.Options = ConvertOptionsV2ToV3(src.Spec.Options) + dst.Spec.HealthCheck = convertHealthCheckToV3(src.Spec.Kubernetes.HealthCheck) + + url, err := CreateBaseURL(pdoknlv3.GetHost(true), "wms", src.Spec.General) + if err != nil { + return err + } + + accessConstraints, err := url.Parse("https://creativecommons.org/publicdomain/zero/1.0/deed.nl") + if err != nil { + return err + } + if src.Spec.Service.AccessConstraints != nil { + accessConstraints, err = url.Parse(*src.Spec.Service.AccessConstraints) + if err != nil { + return err + } + } + + service := pdoknlv3.WMSService{BaseService: pdoknlv3.BaseService{ + Prefix: src.Spec.General.Dataset, + URL: *url, + OwnerInfoRef: "pdok", + Title: fixUnicode(src.Spec.Service.Title), + Abstract: fixUnicode(src.Spec.Service.Abstract), + Keywords: src.Spec.Service.Keywords, + AccessConstraints: smoothoperatormodel.URL{URL: accessConstraints}, + }, + Inspire: nil, + MaxSize: nil, + Resolution: nil, + DefResolution: nil, + DataEPSG: src.Spec.Service.DataEPSG, + Layer: src.Spec.Service.MapLayersToV3(), + } + + if src.Spec.Service.Maxsize != nil { + service.MaxSize = smoothoperatorutils.Pointer(int32(*src.Spec.Service.Maxsize)) + } + + if src.Spec.Service.Resolution != nil { + service.Resolution = smoothoperatorutils.Pointer(int32(*src.Spec.Service.Resolution)) + } + + if src.Spec.Service.DefResolution != nil { + service.DefResolution = smoothoperatorutils.Pointer(int32(*src.Spec.Service.DefResolution)) + } + + if src.Spec.Service.Mapfile != nil { + service.Mapfile = &pdoknlv3.Mapfile{ + ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, + } + } + + if src.Spec.Service.Inspire { + service.Inspire = &pdoknlv3.Inspire{ + ServiceMetadataURL: pdoknlv3.MetadataURL{ + CSW: &pdoknlv3.Metadata{ + MetadataIdentifier: src.Spec.Service.MetadataIdentifier, + }, + }, + Language: "dut", + } + } else { + // Annotation to be able to convert back to v2 + dst.Annotations[ServiceMetatdataIdentifierAnnotation] = src.Spec.Service.MetadataIdentifier + } + + if src.Spec.Service.StylingAssets != nil { + service.StylingAssets = &pdoknlv3.StylingAssets{ + BlobKeys: src.Spec.Service.StylingAssets.BlobKeys, + ConfigMapRefs: []pdoknlv3.ConfigMapRef{}, + } + + for _, cm := range src.Spec.Service.StylingAssets.ConfigMapRefs { + service.StylingAssets.ConfigMapRefs = append(service.StylingAssets.ConfigMapRefs, pdoknlv3.ConfigMapRef{ + Name: cm.Name, + Keys: cm.Keys, + }) + } + + if len(src.Spec.Service.StylingAssets.ConfigMapRefs) == 1 { + for _, layer := range src.Spec.Service.Layers { + for _, style := range layer.Styles { + if style.Visualization != nil && !slices.Contains(service.StylingAssets.ConfigMapRefs[0].Keys, *style.Visualization) { + service.StylingAssets.ConfigMapRefs[0].Keys = append(service.StylingAssets.ConfigMapRefs[0].Keys, *style.Visualization) + } + } + } + } + } + + dst.Spec.Service = service + return nil +} + +func convertHealthCheckToV3(v2 *HealthCheck) *pdoknlv3.HealthCheckWMS { + if v2 != nil { + switch { + case v2.Querystring != nil: + return &pdoknlv3.HealthCheckWMS{ + Querystring: v2.Querystring, + Mimetype: v2.Mimetype, + } + case v2.Boundingbox != nil: + return &pdoknlv3.HealthCheckWMS{ + Boundingbox: smoothoperatorutils.Pointer(smoothoperatormodel.ExtentToBBox(strings.ReplaceAll(*v2.Boundingbox, ",", " "))), + } + } + } - // TODO(user): Implement conversion logic from v2beta1 to v3 return nil } // ConvertFrom converts the Hub version (v3) to this WMS (v2beta1). +// +//nolint:revive func (dst *WMS) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*pdoknlv3.WMS) log.Printf("ConvertFrom: Converting WMS from Hub version v3 to Spoke version v2beta1;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) - // TODO(user): Implement conversion logic from v3 to v2beta1 + dst.ObjectMeta = src.ObjectMeta + + dst.Spec.General = LabelsToV2General(src.ObjectMeta.Labels) + + dst.Spec.Kubernetes = NewV2KubernetesObject(src.Spec.Lifecycle, src.Spec.PodSpecPatch, src.Spec.HorizontalPodAutoscalerPatch) + dst.Spec.Kubernetes.HealthCheck = convertHealthCheckToV2(src.Spec.HealthCheck) + + dst.Spec.Options = ConvertOptionsV3ToV2(src.Spec.Options) + + service := WMSService{ + Title: src.Spec.Service.Title, + Abstract: src.Spec.Service.Abstract, + Keywords: src.Spec.Service.Keywords, + AccessConstraints: ptr.To(src.Spec.Service.AccessConstraints.String()), + Extent: nil, + DataEPSG: src.Spec.Service.DataEPSG, + Layers: []WMSLayer{}, + MetadataIdentifier: "00000000-0000-0000-0000-000000000000", + } + + if src.Spec.Service.Mapfile != nil { + service.Mapfile = &Mapfile{ + ConfigMapKeyRef: src.Spec.Service.Mapfile.ConfigMapKeyRef, + } + } + + if src.Spec.Service.Inspire != nil { + service.Inspire = true + service.MetadataIdentifier = src.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier + } else { + service.Inspire = false + // TODO unable to fill in MetadataIdentifier here until we know how to handle non inspire services + } + + uuid, ok := src.Annotations[ServiceMetatdataIdentifierAnnotation] + if service.MetadataIdentifier == "00000000-0000-0000-0000-000000000000" && ok { + service.MetadataIdentifier = uuid + } + + if src.Spec.Service.DefResolution != nil { + service.DefResolution = smoothoperatorutils.Pointer(int(*src.Spec.Service.DefResolution)) + } + + if src.Spec.Service.Resolution != nil { + service.Resolution = smoothoperatorutils.Pointer(int(*src.Spec.Service.Resolution)) + } + + if src.Spec.Service.StylingAssets != nil { + service.StylingAssets = &StylingAssets{ + BlobKeys: src.Spec.Service.StylingAssets.BlobKeys, + ConfigMapRefs: []ConfigMapRef{}, + } + + for _, cm := range src.Spec.Service.StylingAssets.ConfigMapRefs { + service.StylingAssets.ConfigMapRefs = append(service.StylingAssets.ConfigMapRefs, ConfigMapRef{ + Name: cm.Name, + Keys: cm.Keys, + }) + } + } + + if v3Authority := src.GetAuthority(); v3Authority != nil { + service.Authority = Authority{ + Name: v3Authority.Name, + URL: v3Authority.URL, + } + } + + if src.Spec.Service.MaxSize != nil { + service.Maxsize = smoothoperatorutils.Pointer(float64(*src.Spec.Service.MaxSize)) + } + + service.Layers = mapV3LayerToV2Layers(src.Spec.Service.Layer, nil, src.Spec.Service.DataEPSG) + + // Create BBox that combines all layer bounding boxes + for _, l := range service.Layers { + if l.Extent != nil { + if service.Extent == nil { + service.Extent = l.Extent + } else { + bbox := smoothoperatorutils.Pointer(smoothoperatormodel.ExtentToBBox(*service.Extent)).DeepCopy() + bbox.Combine(smoothoperatormodel.ExtentToBBox(*l.Extent)) + service.Extent = smoothoperatorutils.Pointer(bbox.ToExtent()) + } + } + } + + dst.Spec.Service = service + + return nil +} + +func convertHealthCheckToV2(v3 *pdoknlv3.HealthCheckWMS) *HealthCheck { + if v3 != nil { + switch { + case v3.Querystring != nil: + return &HealthCheck{ + Querystring: v3.Querystring, + Mimetype: v3.Mimetype, + } + case v3.Boundingbox != nil: + return &HealthCheck{ + Boundingbox: smoothoperatorutils.Pointer(strings.ReplaceAll(v3.Boundingbox.ToExtent(), " ", ",")), + } + } + } + return nil } + +func (v2Service WMSService) GetTopLayer() (*WMSLayer, error) { + // Only one layer defined that has data + if len(v2Service.Layers) == 1 && v2Service.Layers[0].Data != nil { + return nil, nil + } + + // If all layers are groupless there is no toplayer + allGroupless := true + for _, layer := range v2Service.Layers { + if layer.Group != nil && *layer.Group != "" { + allGroupless = false + break + } + } + if allGroupless { + return nil, nil + } + + // Some layers have groups defined. + // That means that there must be one layer without a group, that's the top layer + for _, layer := range v2Service.Layers { + if layer.Group == nil || *layer.Group == "" { + return &layer, nil + } + } + + return nil, errors.New("unable to detect the toplayer of this WMS service") +} + +// MapLayersToV3 +func (v2Service WMSService) MapLayersToV3() pdoknlv3.Layer { + // Creates map of Groups: layers in that group + // and a list of all layers without a group + groupedLayers := map[string][]pdoknlv3.Layer{} + var notGroupedLayers []pdoknlv3.Layer + for _, layer := range v2Service.Layers { + if layer.Group == nil { + notGroupedLayers = append(notGroupedLayers, layer.MapToV3(v2Service)) + } else { + groupedLayers[*layer.Group] = append(groupedLayers[*layer.Group], layer.MapToV3(v2Service)) + } + } + + // if a topLayer is defined in the v2 it be the only layer without a group + // and there are other layers that have the topLayer as their group + // and at least one of those layers is itself a group layer + var topLayer *pdoknlv3.Layer + if _, ok := groupedLayers[*notGroupedLayers[0].Name]; ok && len(notGroupedLayers) == 1 { + subLayers := groupedLayers[*notGroupedLayers[0].Name] + ok := false + for _, layer := range subLayers { + if _, ok = groupedLayers[*layer.Name]; ok { + break + } + } + + if ok { + topLayer = ¬GroupedLayers[0] + var bbox *pdoknlv3.WMSBoundingBox + if len(topLayer.BoundingBoxes) > 0 { + bbox = &topLayer.BoundingBoxes[0] + } + topLayer.BoundingBoxes = getDefaultWMSLayerBoundingBoxes(bbox) + } + } + + var middleLayers []pdoknlv3.Layer + + // if the topLayer is not defined in the v2 layers + // it needs to be created with defaults from the service + // and in this case the middleLayers are all layers without a group + if topLayer == nil { + var bbox *pdoknlv3.WMSBoundingBox + if v2Service.Extent != nil { + bboxStringList := strings.Split(*v2Service.Extent, " ") + bbox = &pdoknlv3.WMSBoundingBox{ + CRS: v2Service.DataEPSG, + BBox: smoothoperatormodel.BBox{ + MinX: bboxStringList[0], + MaxX: bboxStringList[2], + MinY: bboxStringList[1], + MaxY: bboxStringList[3], + }, + } + } + + topLayer = &pdoknlv3.Layer{ + Title: smoothoperatorutils.Pointer(fixUnicode(v2Service.Title)), + Abstract: smoothoperatorutils.Pointer(fixUnicode(v2Service.Abstract)), + Keywords: v2Service.Keywords, + Layers: []pdoknlv3.Layer{}, + BoundingBoxes: getDefaultWMSLayerBoundingBoxes(bbox), + Visible: true, + } + + // adding the bottom layers to the middle layers they are grouped by + for _, layer := range notGroupedLayers { + bottomLayers := groupedLayers[*layer.Name] + layer.Layers = bottomLayers + middleLayers = append(middleLayers, layer) + } + } + + // if the topLayer is defined in the v2 layers + // meaning the topLayer has a name at this point + // then the middleLayers are all layers that had the topLayer name as their group + // and the bottomLayers are all layers that had a middleLayer as a group + if topLayer.Name != nil { + for _, layer := range groupedLayers[*topLayer.Name] { + bottomLayers := groupedLayers[*layer.Name] + layer.Layers = bottomLayers + middleLayers = append(middleLayers, layer) + } + } + topLayer.Layers = middleLayers + + return *topLayer +} + +func getDefaultWMSLayerBoundingBoxes(defaultBbox *pdoknlv3.WMSBoundingBox) []pdoknlv3.WMSBoundingBox { + defaultBboxes := []pdoknlv3.WMSBoundingBox{ + { + CRS: "EPSG:28992", + BBox: smoothoperatormodel.BBox{ + MinX: "-25000", + MinY: "250000", + MaxX: "280000", + MaxY: "860000", + }, + }, + { + CRS: "EPSG:25831", + BBox: smoothoperatormodel.BBox{ + MinX: "-470271", + MinY: "5562310", + MaxX: "795163", + MaxY: "6181970", + }, + }, + { + CRS: "EPSG:25832", + BBox: smoothoperatormodel.BBox{ + MinX: "62461.6", + MinY: "5565550", + MaxX: "397827", + MaxY: "6190420", + }, + }, + { + CRS: "EPSG:3034", + BBox: smoothoperatormodel.BBox{ + MinX: "2613360", + MinY: "3509000", + MaxX: "3220070", + MaxY: "3840030", + }, + }, + { + CRS: "EPSG:3035", + BBox: smoothoperatormodel.BBox{ + MinX: "3016760", + MinY: "3812640", + MaxX: "3644850", + MaxY: "4155860", + }, + }, + { + CRS: "EPSG:3857", + BBox: smoothoperatormodel.BBox{ + MinX: "281318", + MinY: "6483220", + MaxX: "820873", + MaxY: "7503110", + }, + }, + { + CRS: "EPSG:4258", + BBox: smoothoperatormodel.BBox{ + MinX: "50.2129", + MinY: "2.52713", + MaxX: "55.7212", + MaxY: "7.37403", + }, + }, + { + CRS: "EPSG:4326", + BBox: smoothoperatormodel.BBox{ + MinX: "50.2129", + MinY: "2.52713", + MaxX: "55.7212", + MaxY: "7.37403", + }, + }, + { + CRS: "CRS:84", + BBox: smoothoperatormodel.BBox{ + MinX: "2.52713", + MinY: "50.2129", + MaxX: "7.37403", + MaxY: "55.7212", + }, + }, + } + bboxes := []pdoknlv3.WMSBoundingBox{} + if defaultBbox != nil { + bboxes = []pdoknlv3.WMSBoundingBox{*defaultBbox} + } + for _, bbox := range defaultBboxes { + if defaultBbox == nil || bbox.CRS != defaultBbox.CRS { + bboxes = append(bboxes, bbox) + } + } + return bboxes +} + +func (v2Layer WMSLayer) MapToV3(v2Service WMSService) pdoknlv3.Layer { + var abstract *string + if v2Layer.Abstract != nil { + abstract = smoothoperatorutils.Pointer(fixUnicode(*v2Layer.Abstract)) + } + layer := pdoknlv3.Layer{ + Name: &v2Layer.Name, + Title: v2Layer.Title, + Abstract: abstract, + Keywords: v2Layer.Keywords, + LabelNoClip: v2Layer.LabelNoClip, + Styles: []pdoknlv3.Style{}, + Layers: nil, + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + MinScaleDenominator: nil, + MaxScaleDenominator: nil, + Visible: smoothoperatorutils.PointerVal(v2Layer.Visible, true), + } + + if v2Layer.SourceMetadataIdentifier != nil { + layer.Authority = &pdoknlv3.Authority{ + Name: v2Service.Authority.Name, + URL: v2Service.Authority.URL, + SpatialDatasetIdentifier: *v2Layer.SourceMetadataIdentifier, + } + } + + if v2Layer.DatasetMetadataIdentifier != nil { + layer.DatasetMetadataURL = &pdoknlv3.MetadataURL{ + CSW: &pdoknlv3.Metadata{ + MetadataIdentifier: *v2Layer.DatasetMetadataIdentifier, + }, + } + } + + if v2Layer.Extent != nil { + layer.BoundingBoxes = append(layer.BoundingBoxes, pdoknlv3.WMSBoundingBox{ + CRS: v2Service.DataEPSG, + BBox: smoothoperatormodel.ExtentToBBox(*v2Layer.Extent), + }) + } else if v2Service.Extent != nil { + layer.BoundingBoxes = append(layer.BoundingBoxes, pdoknlv3.WMSBoundingBox{ + CRS: v2Service.DataEPSG, + BBox: smoothoperatormodel.ExtentToBBox(*v2Service.Extent), + }) + } + + if v2Layer.MinScale != nil { + layer.MinScaleDenominator = smoothoperatorutils.Pointer(strconv.FormatFloat(*v2Layer.MinScale, 'f', -1, 64)) + } + + if v2Layer.MaxScale != nil { + layer.MaxScaleDenominator = smoothoperatorutils.Pointer(strconv.FormatFloat(*v2Layer.MaxScale, 'f', -1, 64)) + } + + for _, style := range v2Layer.Styles { + v3Style := pdoknlv3.Style{ + Name: style.Name, + Title: style.Title, + Abstract: style.Abstract, + Visualization: style.Visualization, + } + + if style.LegendFile != nil { + v3Style.Legend = &pdoknlv3.Legend{ + BlobKey: style.LegendFile.BlobKey, + } + } + + layer.Styles = append(layer.Styles, v3Style) + } + + if v2Layer.Data != nil { + layer.Data = smoothoperatorutils.Pointer(ConvertV2DataToV3(*v2Layer.Data)) + } + + return layer +} + +//nolint:cyclop +func mapV3LayerToV2Layers(v3Layer pdoknlv3.Layer, parent *pdoknlv3.Layer, serviceEPSG string) []WMSLayer { + var layers []WMSLayer + + //nolint:nestif + if parent == nil && v3Layer.Name == nil { + // Default top layer, do not include in v2 layers + if v3Layer.Layers != nil { + for _, childLayer := range v3Layer.Layers { + layers = append(layers, mapV3LayerToV2Layers(childLayer, nil, serviceEPSG)...) + } + } + } else { + v2Layer := WMSLayer{ + Name: *v3Layer.Name, + Title: v3Layer.Title, + Abstract: v3Layer.Abstract, + Keywords: v3Layer.Keywords, + LabelNoClip: v3Layer.LabelNoClip, + Styles: []Style{}, + } + + v2Layer.Visible = &v3Layer.Visible + + if parent != nil { + v2Layer.Group = parent.Name + } + + if v3Layer.DatasetMetadataURL != nil && v3Layer.DatasetMetadataURL.CSW != nil { + v2Layer.DatasetMetadataIdentifier = &v3Layer.DatasetMetadataURL.CSW.MetadataIdentifier + } + + if v3Layer.Authority != nil { + v2Layer.SourceMetadataIdentifier = &v3Layer.Authority.SpatialDatasetIdentifier + } + + for _, bb := range v3Layer.BoundingBoxes { + if bb.CRS == serviceEPSG { + v2Layer.Extent = smoothoperatorutils.Pointer(bb.BBox.ToExtent()) + } + } + + if v3Layer.MinScaleDenominator != nil { + val, err := strconv.ParseFloat(*v3Layer.MinScaleDenominator, 64) + if err != nil { + panic(err) + } + v2Layer.MinScale = &val + } + + if v3Layer.MaxScaleDenominator != nil { + val, err := strconv.ParseFloat(*v3Layer.MaxScaleDenominator, 64) + if err != nil { + panic(err) + } + v2Layer.MaxScale = &val + } + + for _, v3Style := range v3Layer.Styles { + v2Style := Style{ + Name: v3Style.Name, + Title: v3Style.Title, + Abstract: v3Style.Abstract, + Visualization: v3Style.Visualization, + } + + if v3Style.Legend != nil { + v2Style.LegendFile = &LegendFile{ + BlobKey: v3Style.Legend.BlobKey, + } + } + + v2Layer.Styles = append(v2Layer.Styles, v2Style) + } + + if v3Layer.Data != nil { + v2Layer.Data = smoothoperatorutils.Pointer(ConvertV3DataToV2(*v3Layer.Data)) + } + + layers = append(layers, v2Layer) + + if v3Layer.Layers != nil { + for _, childLayer := range v3Layer.Layers { + layers = append(layers, mapV3LayerToV2Layers(childLayer, &v3Layer, serviceEPSG)...) + } + } + } + + return layers +} diff --git a/api/v2beta1/wms_conversion_test.go b/api/v2beta1/wms_conversion_test.go new file mode 100644 index 0000000..0305958 --- /dev/null +++ b/api/v2beta1/wms_conversion_test.go @@ -0,0 +1,232 @@ +package v2beta1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "k8s.io/utils/ptr" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" +) + +func TestV2ToV3(t *testing.T) { + //nolint:misspell + input := "apiVersion: pdok.nl/v2beta1\nkind: WMS\nmetadata:\n name: rws-nwbwegen-v1-0\n labels:\n dataset-owner: rws\n dataset: nwbwegen\n service-version: v1_0\n service-type: wms\n annotations:\n lifecycle-phase: prod\n service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb\nspec:\n general:\n datasetOwner: rws\n dataset: nwbwegen\n serviceVersion: v1_0\n kubernetes:\n healthCheck:\n boundingbox: 135134.89,457152.55,135416.03,457187.82\n resources:\n limits:\n ephemeralStorage: 1535Mi\n memory: 4G\n requests:\n cpu: 2000m\n ephemeralStorage: 1535Mi\n memory: 4G\n options:\n automaticCasing: true\n disableWebserviceProxy: false\n includeIngress: true\n validateRequests: true\n service:\n title: NWB - Wegen WMS\n abstract:\n Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen.\n Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen\n Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland.\n Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk,\n provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien\n van een straatnaam of nummer.\n authority:\n name: rws\n url: https://www.rijkswaterstaat.nl\n dataEPSG: EPSG:28992\n extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961\n inspire: true\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n - Hectometerpunten\n - HVD\n - Mobiliteit\n stylingAssets:\n configMapRefs:\n - name: includes\n keys:\n - nwb_wegen_hectopunten.symbol\n - hectopunten.style\n - wegvakken.style\n blobKeys:\n - resources/fonts/liberation-sans.ttf\n layers:\n - abstract:\n Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB)\n en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer,\n routenummer, wegbeheerder, huisnummers, enz. weer.\n data:\n gpkg:\n columns:\n - objectid\n - wvk_id\n - wvk_begdat\n - jte_id_beg\n - jte_id_end\n - wegbehsrt\n - wegnummer\n - wegdeelltr\n - hecto_lttr\n - bst_code\n - rpe_code\n - admrichtng\n - rijrichtng\n - stt_naam\n - stt_bron\n - wpsnaam\n - gme_id\n - gme_naam\n - hnrstrlnks\n - hnrstrrhts\n - e_hnr_lnks\n - e_hnr_rhts\n - l_hnr_lnks\n - l_hnr_rhts\n - begafstand\n - endafstand\n - beginkm\n - eindkm\n - pos_tv_wol\n - wegbehcode\n - wegbehnaam\n - distrcode\n - distrnaam\n - dienstcode\n - dienstnaam\n - wegtype\n - wgtype_oms\n - routeltr\n - routenr\n - routeltr2\n - routenr2\n - routeltr3\n - routenr3\n - routeltr4\n - routenr4\n - wegnr_aw\n - wegnr_hmp\n - geobron_id\n - geobron_nm\n - bronjaar\n - openlr\n - bag_orl\n - frc\n - fow\n - alt_naam\n - alt_nr\n - rel_hoogte\n - st_lengthshape\n geometryType: MultiLineString\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: wegvakken\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n maxScale: 50000.0\n minScale: 1.0\n name: wegvakken\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: wegvakken\n title: NWB - Wegvakken\n visualization: wegvakken.style\n title: Wegvakken\n visible: true\n - abstract:\n Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB)\n en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand,\n zijde en hectoletter weer.\n data:\n gpkg:\n columns:\n - objectid\n - hectomtrng\n - afstand\n - wvk_id\n - wvk_begdat\n - zijde\n - hecto_lttr\n geometryType: MultiPoint\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: hectopunten\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Hectometerpunten\n maxScale: 50000.0\n minScale: 1.0\n name: hectopunten\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: hectopunten\n title: NWB - Hectopunten\n visualization: hectopunten.style\n title: Hectopunten\n visible: true\n metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8\n" + v2wms := &WMS{} + err := yaml.Unmarshal([]byte(input), v2wms) + assert.NoError(t, err) + var target pdoknlv3.WMS + err = v2wms.ToV3(&target) + assert.NoError(t, err) + assert.Equal(t, "NWB - Wegen WMS", target.Spec.Service.Title) + a := 0 + _ = a +} + +func TestWMSService_MapLayersToV3(t *testing.T) { + tests := []struct { + name string + v2Service WMSService + want pdoknlv3.Layer + }{ + { + name: "no toplayer, middle: 1 data layer", + v2Service: WMSService{Layers: []WMSLayer{ + {Name: "layer"}, + }}, + want: pdoknlv3.Layer{ + Title: ptr.To(""), + Abstract: ptr.To(""), + BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), + Visible: true, + Layers: []pdoknlv3.Layer{{ + Name: ptr.To("layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }}, + }, + }, + { + name: "no toplayer, middle: 1 group layer", + v2Service: WMSService{Layers: []WMSLayer{ + {Name: "group-layer"}, + {Name: "sub-layer", Group: ptr.To("group-layer")}, + }}, + want: pdoknlv3.Layer{ + Title: ptr.To(""), + Abstract: ptr.To(""), + BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), + Visible: true, + Layers: []pdoknlv3.Layer{{ + Name: ptr.To("group-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("sub-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }}, + }, + }, + { + name: "no toplayer, middle: 2 group layers", + v2Service: WMSService{Layers: []WMSLayer{ + {Name: "group-layer-1"}, + {Name: "sub-layer-1", Group: ptr.To("group-layer-1")}, + {Name: "group-layer-2"}, + {Name: "sub-layer-2", Group: ptr.To("group-layer-2")}, + }}, + want: pdoknlv3.Layer{ + Title: ptr.To(""), + Abstract: ptr.To(""), + BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), + Visible: true, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("group-layer-1"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("sub-layer-1"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }, + { + Name: ptr.To("group-layer-2"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("sub-layer-2"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }, + }, + }, + }, + { + name: "no toplayer, middle: 1 group layer, 1 data layer", + v2Service: WMSService{Layers: []WMSLayer{ + {Name: "group-layer"}, + {Name: "sub-layer", Group: ptr.To("group-layer")}, + {Name: "data-layer"}, + }}, + want: pdoknlv3.Layer{ + Title: ptr.To(""), + Abstract: ptr.To(""), + BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), + Visible: true, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("group-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("sub-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }, + { + Name: ptr.To("data-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }, + }, + { + name: "toplayer, middle: 1 group layer", + v2Service: WMSService{Layers: []WMSLayer{ + {Name: "group-layer", Group: ptr.To("top-layer")}, + {Name: "sub-layer", Group: ptr.To("group-layer")}, + {Name: "top-layer"}, + }}, + want: pdoknlv3.Layer{ + Name: ptr.To("top-layer"), + BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{{ + Name: ptr.To("group-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("sub-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }}, + }, + }, + { + name: "toplayer, middle: 1 group layer, 1 data layer", + v2Service: WMSService{Layers: []WMSLayer{ + {Name: "group-layer", Group: ptr.To("top-layer")}, + {Name: "sub-layer", Group: ptr.To("group-layer")}, + {Name: "top-layer"}, + {Name: "data-layer", Group: ptr.To("top-layer")}, + }}, + want: pdoknlv3.Layer{ + Name: ptr.To("top-layer"), + BoundingBoxes: getDefaultWMSLayerBoundingBoxes(nil), + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("group-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + Layers: []pdoknlv3.Layer{ + { + Name: ptr.To("sub-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }, + { + Name: ptr.To("data-layer"), + BoundingBoxes: []pdoknlv3.WMSBoundingBox{}, + Visible: true, + Styles: []pdoknlv3.Style{}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + diff := cmp.Diff(tt.want, tt.v2Service.MapLayersToV3()) + assert.Equal(t, diff == "", true, "%s", diff) + }) + } +} diff --git a/api/v2beta1/wms_types.go b/api/v2beta1/wms_types.go index f5918a6..2d7a160 100644 --- a/api/v2beta1/wms_types.go +++ b/api/v2beta1/wms_types.go @@ -1,53 +1,126 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v2beta1 import ( + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -// WMSSpec defines the desired state of WMS. +// +kubebuilder:object:root=true +// +kubebuilder:skipversion + +// WMS is the Schema for the wms API. +type WMS struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec WMSSpec `json:"spec,omitempty"` + Status *Status `json:"status,omitempty"` +} + +// WMSSpec is the struct for all fields defined in the WMS CRD type WMSSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + General General `json:"general"` + Service WMSService `json:"service"` + Options *WMSWFSOptions `json:"options,omitempty"` + Kubernetes Kubernetes `json:"kubernetes"` +} - // Foo is an example field of WMS. Edit wms_types.go to remove/update - Foo string `json:"foo,omitempty"` +// WMSService is the struct for all service level fields +type WMSService struct { + Inspire bool `json:"inspire,omitempty"` + Title string `json:"title"` + Abstract string `json:"abstract"` + // +kubebuilder:default="https://creativecommons.org/publicdomain/zero/1.0/deed.nl" + AccessConstraints *string `json:"accessConstraints,omitempty"` // Pointer for CRD conversion as defaulting is not applied there + Keywords []string `json:"keywords"` + MetadataIdentifier string `json:"metadataIdentifier"` + Authority Authority `json:"authority"` + Layers []WMSLayer `json:"layers"` + //nolint:tagliatelle + DataEPSG string `json:"dataEPSG"` + Extent *string `json:"extent,omitempty"` + Maxsize *float64 `json:"maxSize,omitempty"` + Resolution *int `json:"resolution,omitempty"` + DefResolution *int `json:"defResolution,omitempty"` + StylingAssets *StylingAssets `json:"stylingAssets,omitempty"` + Mapfile *Mapfile `json:"mapfile,omitempty"` } -// WMSStatus defines the observed state of WMS. -type WMSStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file +// WMSLayer is the struct for all layer level fields +type WMSLayer struct { + Name string `json:"name"` + Group *string `json:"group,omitempty"` + Visible *bool `json:"visible,omitempty"` + Title *string `json:"title,omitempty"` + Abstract *string `json:"abstract,omitempty"` + Keywords []string `json:"keywords,omitempty"` + DatasetMetadataIdentifier *string `json:"datasetMetadataIdentifier,omitempty"` + SourceMetadataIdentifier *string `json:"sourceMetadataIdentifier,omitempty"` + Styles []Style `json:"styles"` + Extent *string `json:"extent,omitempty"` + MinScale *float64 `json:"minScale,omitempty"` + MaxScale *float64 `json:"maxScale,omitempty"` + LabelNoClip bool `json:"labelNoClip,omitempty"` + Data *Data `json:"data,omitempty"` } -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status +// Style is the struct for all style level fields +type Style struct { + Name string `json:"name"` + Title *string `json:"title,omitempty"` + Abstract *string `json:"abstract,omitempty"` + Visualization *string `json:"visualization,omitempty"` + LegendFile *LegendFile `json:"legendFile,omitempty"` +} -// WMS is the Schema for the wms API. -type WMS struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` +// LegendFile is the struct containing the location of the legendfile +type LegendFile struct { + BlobKey string `json:"blobKey"` +} + +// StylingAssets is the struct containing the location of styling assets +type StylingAssets struct { + ConfigMapRefs []ConfigMapRef `json:"configMapRefs,omitempty"` + BlobKeys []string `json:"blobKeys,omitempty"` +} + +// ConfigMapRef contains all the config map name and all keys in that mapserver that are relevant +// the Keys can be empty, so that the v1 WMS can convert to the v2beta1 WMS +type ConfigMapRef struct { + Name string `json:"name"` + Keys []string `json:"keys,omitempty"` +} - Spec WMSSpec `json:"spec,omitempty"` - Status WMSStatus `json:"status,omitempty"` +// Mapfile contains the ConfigMapKeyRef containing a mapfile +type Mapfile struct { + ConfigMapKeyRef corev1.ConfigMapKeySelector `json:"configMapKeyRef"` } // +kubebuilder:object:root=true diff --git a/api/v2beta1/zz_generated.deepcopy.go b/api/v2beta1/zz_generated.deepcopy.go index 4975d06..7adf7ea 100644 --- a/api/v2beta1/zz_generated.deepcopy.go +++ b/api/v2beta1/zz_generated.deepcopy.go @@ -1,19 +1,27 @@ //go:build !ignore_autogenerated /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ // Code generated by controller-gen. DO NOT EDIT. @@ -21,16 +29,534 @@ limitations under the License. package v2beta1 import ( + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authority) DeepCopyInto(out *Authority) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authority. +func (in *Authority) DeepCopy() *Authority { + if in == nil { + return nil + } + out := new(Authority) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Autoscaling) DeepCopyInto(out *Autoscaling) { + *out = *in + if in.AverageCPUUtilization != nil { + in, out := &in.AverageCPUUtilization, &out.AverageCPUUtilization + *out = new(int) + **out = **in + } + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int) + **out = **in + } + if in.MaxReplicas != nil { + in, out := &in.MaxReplicas, &out.MaxReplicas + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Autoscaling. +func (in *Autoscaling) DeepCopy() *Autoscaling { + if in == nil { + return nil + } + out := new(Autoscaling) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + if in.AnsibleResult != nil { + in, out := &in.AnsibleResult, &out.AnsibleResult + *out = new(ResultAnsible) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMapRef) DeepCopyInto(out *ConfigMapRef) { + *out = *in + if in.Keys != nil { + in, out := &in.Keys, &out.Keys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapRef. +func (in *ConfigMapRef) DeepCopy() *ConfigMapRef { + if in == nil { + return nil + } + out := new(ConfigMapRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Data) DeepCopyInto(out *Data) { + *out = *in + if in.GPKG != nil { + in, out := &in.GPKG, &out.GPKG + *out = new(GPKG) + (*in).DeepCopyInto(*out) + } + if in.Postgis != nil { + in, out := &in.Postgis, &out.Postgis + *out = new(Postgis) + (*in).DeepCopyInto(*out) + } + if in.Tif != nil { + in, out := &in.Tif, &out.Tif + *out = new(Tif) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Data. +func (in *Data) DeepCopy() *Data { + if in == nil { + return nil + } + out := new(Data) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureType) DeepCopyInto(out *FeatureType) { + *out = *in + if in.Keywords != nil { + in, out := &in.Keywords, &out.Keywords + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Extent != nil { + in, out := &in.Extent, &out.Extent + *out = new(string) + **out = **in + } + in.Data.DeepCopyInto(&out.Data) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureType. +func (in *FeatureType) DeepCopy() *FeatureType { + if in == nil { + return nil + } + out := new(FeatureType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GPKG) DeepCopyInto(out *GPKG) { + *out = *in + if in.Columns != nil { + in, out := &in.Columns, &out.Columns + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Aliases != nil { + in, out := &in.Aliases, &out.Aliases + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GPKG. +func (in *GPKG) DeepCopy() *GPKG { + if in == nil { + return nil + } + out := new(GPKG) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *General) DeepCopyInto(out *General) { + *out = *in + if in.Theme != nil { + in, out := &in.Theme, &out.Theme + *out = new(string) + **out = **in + } + if in.ServiceVersion != nil { + in, out := &in.ServiceVersion, &out.ServiceVersion + *out = new(string) + **out = **in + } + if in.DataVersion != nil { + in, out := &in.DataVersion, &out.DataVersion + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new General. +func (in *General) DeepCopy() *General { + if in == nil { + return nil + } + out := new(General) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthCheck) DeepCopyInto(out *HealthCheck) { + *out = *in + if in.Querystring != nil { + in, out := &in.Querystring, &out.Querystring + *out = new(string) + **out = **in + } + if in.Mimetype != nil { + in, out := &in.Mimetype, &out.Mimetype + *out = new(string) + **out = **in + } + if in.Boundingbox != nil { + in, out := &in.Boundingbox, &out.Boundingbox + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheck. +func (in *HealthCheck) DeepCopy() *HealthCheck { + if in == nil { + return nil + } + out := new(HealthCheck) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Kubernetes) DeepCopyInto(out *Kubernetes) { + *out = *in + if in.Autoscaling != nil { + in, out := &in.Autoscaling, &out.Autoscaling + *out = new(Autoscaling) + (*in).DeepCopyInto(*out) + } + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(HealthCheck) + (*in).DeepCopyInto(*out) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(v1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.Lifecycle != nil { + in, out := &in.Lifecycle, &out.Lifecycle + *out = new(Lifecycle) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kubernetes. +func (in *Kubernetes) DeepCopy() *Kubernetes { + if in == nil { + return nil + } + out := new(Kubernetes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LegendFile) DeepCopyInto(out *LegendFile) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LegendFile. +func (in *LegendFile) DeepCopy() *LegendFile { + if in == nil { + return nil + } + out := new(LegendFile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Lifecycle) DeepCopyInto(out *Lifecycle) { + *out = *in + if in.TTLInDays != nil { + in, out := &in.TTLInDays, &out.TTLInDays + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Lifecycle. +func (in *Lifecycle) DeepCopy() *Lifecycle { + if in == nil { + return nil + } + out := new(Lifecycle) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mapfile) DeepCopyInto(out *Mapfile) { + *out = *in + in.ConfigMapKeyRef.DeepCopyInto(&out.ConfigMapKeyRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mapfile. +func (in *Mapfile) DeepCopy() *Mapfile { + if in == nil { + return nil + } + out := new(Mapfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Postgis) DeepCopyInto(out *Postgis) { + *out = *in + if in.Columns != nil { + in, out := &in.Columns, &out.Columns + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Aliases != nil { + in, out := &in.Aliases, &out.Aliases + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgis. +func (in *Postgis) DeepCopy() *Postgis { + if in == nil { + return nil + } + out := new(Postgis) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Resources) DeepCopyInto(out *Resources) { + *out = *in + if in.APIVersion != nil { + in, out := &in.APIVersion, &out.APIVersion + *out = new(string) + **out = **in + } + if in.Kind != nil { + in, out := &in.Kind, &out.Kind + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resources. +func (in *Resources) DeepCopy() *Resources { + if in == nil { + return nil + } + out := new(Resources) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResultAnsible) DeepCopyInto(out *ResultAnsible) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResultAnsible. +func (in *ResultAnsible) DeepCopy() *ResultAnsible { + if in == nil { + return nil + } + out := new(ResultAnsible) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Status) DeepCopyInto(out *Status) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Deployment != nil { + in, out := &in.Deployment, &out.Deployment + *out = new(string) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]Resources, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. +func (in *Status) DeepCopy() *Status { + if in == nil { + return nil + } + out := new(Status) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Style) DeepCopyInto(out *Style) { + *out = *in + if in.Title != nil { + in, out := &in.Title, &out.Title + *out = new(string) + **out = **in + } + if in.Abstract != nil { + in, out := &in.Abstract, &out.Abstract + *out = new(string) + **out = **in + } + if in.Visualization != nil { + in, out := &in.Visualization, &out.Visualization + *out = new(string) + **out = **in + } + if in.LegendFile != nil { + in, out := &in.LegendFile, &out.LegendFile + *out = new(LegendFile) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Style. +func (in *Style) DeepCopy() *Style { + if in == nil { + return nil + } + out := new(Style) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StylingAssets) DeepCopyInto(out *StylingAssets) { + *out = *in + if in.ConfigMapRefs != nil { + in, out := &in.ConfigMapRefs, &out.ConfigMapRefs + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.BlobKeys != nil { + in, out := &in.BlobKeys, &out.BlobKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StylingAssets. +func (in *StylingAssets) DeepCopy() *StylingAssets { + if in == nil { + return nil + } + out := new(StylingAssets) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Tif) DeepCopyInto(out *Tif) { + *out = *in + if in.GetFeatureInfoIncludesClass != nil { + in, out := &in.GetFeatureInfoIncludesClass, &out.GetFeatureInfoIncludesClass + *out = new(bool) + **out = **in + } + if in.Offsite != nil { + in, out := &in.Offsite, &out.Offsite + *out = new(string) + **out = **in + } + if in.Resample != nil { + in, out := &in.Resample, &out.Resample + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tif. +func (in *Tif) DeepCopy() *Tif { + if in == nil { + return nil + } + out := new(Tif) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WFS) DeepCopyInto(out *WFS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(Status) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFS. @@ -84,31 +610,72 @@ func (in *WFSList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { +func (in *WFSService) DeepCopyInto(out *WFSService) { *out = *in + if in.AccessConstraints != nil { + in, out := &in.AccessConstraints, &out.AccessConstraints + *out = new(string) + **out = **in + } + if in.Keywords != nil { + in, out := &in.Keywords, &out.Keywords + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.Authority = in.Authority + if in.Extent != nil { + in, out := &in.Extent, &out.Extent + *out = new(string) + **out = **in + } + if in.Maxfeatures != nil { + in, out := &in.Maxfeatures, &out.Maxfeatures + *out = new(string) + **out = **in + } + if in.FeatureTypes != nil { + in, out := &in.FeatureTypes, &out.FeatureTypes + *out = make([]FeatureType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Mapfile != nil { + in, out := &in.Mapfile, &out.Mapfile + *out = new(Mapfile) + (*in).DeepCopyInto(*out) + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. -func (in *WFSSpec) DeepCopy() *WFSSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSService. +func (in *WFSService) DeepCopy() *WFSService { if in == nil { return nil } - out := new(WFSSpec) + out := new(WFSService) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSStatus) DeepCopyInto(out *WFSStatus) { +func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { *out = *in + in.General.DeepCopyInto(&out.General) + in.Service.DeepCopyInto(&out.Service) + in.Kubernetes.DeepCopyInto(&out.Kubernetes) + if in.Options != nil { + in, out := &in.Options, &out.Options + *out = new(WMSWFSOptions) + (*in).DeepCopyInto(*out) + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSStatus. -func (in *WFSStatus) DeepCopy() *WFSStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. +func (in *WFSSpec) DeepCopy() *WFSSpec { if in == nil { return nil } - out := new(WFSStatus) + out := new(WFSSpec) in.DeepCopyInto(out) return out } @@ -118,8 +685,12 @@ func (in *WMS) DeepCopyInto(out *WMS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + if in.Status != nil { + in, out := &in.Status, &out.Status + *out = new(Status) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMS. @@ -140,6 +711,83 @@ func (in *WMS) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WMSLayer) DeepCopyInto(out *WMSLayer) { + *out = *in + if in.Group != nil { + in, out := &in.Group, &out.Group + *out = new(string) + **out = **in + } + if in.Visible != nil { + in, out := &in.Visible, &out.Visible + *out = new(bool) + **out = **in + } + if in.Title != nil { + in, out := &in.Title, &out.Title + *out = new(string) + **out = **in + } + if in.Abstract != nil { + in, out := &in.Abstract, &out.Abstract + *out = new(string) + **out = **in + } + if in.Keywords != nil { + in, out := &in.Keywords, &out.Keywords + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DatasetMetadataIdentifier != nil { + in, out := &in.DatasetMetadataIdentifier, &out.DatasetMetadataIdentifier + *out = new(string) + **out = **in + } + if in.SourceMetadataIdentifier != nil { + in, out := &in.SourceMetadataIdentifier, &out.SourceMetadataIdentifier + *out = new(string) + **out = **in + } + if in.Styles != nil { + in, out := &in.Styles, &out.Styles + *out = make([]Style, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Extent != nil { + in, out := &in.Extent, &out.Extent + *out = new(string) + **out = **in + } + if in.MinScale != nil { + in, out := &in.MinScale, &out.MinScale + *out = new(float64) + **out = **in + } + if in.MaxScale != nil { + in, out := &in.MaxScale, &out.MaxScale + *out = new(float64) + **out = **in + } + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = new(Data) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSLayer. +func (in *WMSLayer) DeepCopy() *WMSLayer { + if in == nil { + return nil + } + out := new(WMSLayer) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WMSList) DeepCopyInto(out *WMSList) { *out = *in @@ -172,9 +820,80 @@ func (in *WMSList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WMSService) DeepCopyInto(out *WMSService) { + *out = *in + if in.AccessConstraints != nil { + in, out := &in.AccessConstraints, &out.AccessConstraints + *out = new(string) + **out = **in + } + if in.Keywords != nil { + in, out := &in.Keywords, &out.Keywords + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.Authority = in.Authority + if in.Layers != nil { + in, out := &in.Layers, &out.Layers + *out = make([]WMSLayer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Extent != nil { + in, out := &in.Extent, &out.Extent + *out = new(string) + **out = **in + } + if in.Maxsize != nil { + in, out := &in.Maxsize, &out.Maxsize + *out = new(float64) + **out = **in + } + if in.Resolution != nil { + in, out := &in.Resolution, &out.Resolution + *out = new(int) + **out = **in + } + if in.DefResolution != nil { + in, out := &in.DefResolution, &out.DefResolution + *out = new(int) + **out = **in + } + if in.StylingAssets != nil { + in, out := &in.StylingAssets, &out.StylingAssets + *out = new(StylingAssets) + (*in).DeepCopyInto(*out) + } + if in.Mapfile != nil { + in, out := &in.Mapfile, &out.Mapfile + *out = new(Mapfile) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSService. +func (in *WMSService) DeepCopy() *WMSService { + if in == nil { + return nil + } + out := new(WMSService) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WMSSpec) DeepCopyInto(out *WMSSpec) { *out = *in + in.General.DeepCopyInto(&out.General) + in.Service.DeepCopyInto(&out.Service) + if in.Options != nil { + in, out := &in.Options, &out.Options + *out = new(WMSWFSOptions) + (*in).DeepCopyInto(*out) + } + in.Kubernetes.DeepCopyInto(&out.Kubernetes) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSSpec. @@ -188,16 +907,41 @@ func (in *WMSSpec) DeepCopy() *WMSSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSStatus) DeepCopyInto(out *WMSStatus) { +func (in *WMSWFSOptions) DeepCopyInto(out *WMSWFSOptions) { *out = *in + if in.ValidateRequests != nil { + in, out := &in.ValidateRequests, &out.ValidateRequests + *out = new(bool) + **out = **in + } + if in.RewriteGroupToDataLayers != nil { + in, out := &in.RewriteGroupToDataLayers, &out.RewriteGroupToDataLayers + *out = new(bool) + **out = **in + } + if in.DisableWebserviceProxy != nil { + in, out := &in.DisableWebserviceProxy, &out.DisableWebserviceProxy + *out = new(bool) + **out = **in + } + if in.PrefetchData != nil { + in, out := &in.PrefetchData, &out.PrefetchData + *out = new(bool) + **out = **in + } + if in.ValidateChildStyleNameEqual != nil { + in, out := &in.ValidateChildStyleNameEqual, &out.ValidateChildStyleNameEqual + *out = new(bool) + **out = **in + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSStatus. -func (in *WMSStatus) DeepCopy() *WMSStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSWFSOptions. +func (in *WMSWFSOptions) DeepCopy() *WMSWFSOptions { if in == nil { return nil } - out := new(WMSStatus) + out := new(WMSWFSOptions) in.DeepCopyInto(out) return out } diff --git a/api/v3/groupversion_info.go b/api/v3/groupversion_info.go index a616013..9262d83 100644 --- a/api/v3/groupversion_info.go +++ b/api/v3/groupversion_info.go @@ -1,17 +1,25 @@ /* -Copyright 2025. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ // Package v3 contains API Schema definitions for the v3 API group. @@ -20,6 +28,7 @@ limitations under the License. package v3 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) @@ -33,4 +42,14 @@ var ( // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme + + TypeMetaWFS = metav1.TypeMeta{ + Kind: "WFS", + APIVersion: GroupVersion.String(), + } + + TypeMetaWMS = metav1.TypeMeta{ + Kind: "WMS", + APIVersion: GroupVersion.String(), + } ) diff --git a/api/v3/shared_types.go b/api/v3/shared_types.go new file mode 100644 index 0000000..ff27831 --- /dev/null +++ b/api/v3/shared_types.go @@ -0,0 +1,329 @@ +package v3 + +import ( + "strings" + + smoothoperatormodel "github.com/pdok/smooth-operator/model" + + "k8s.io/apimachinery/pkg/runtime/schema" + + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var host string + +type ServiceType string + +const ( + ServiceTypeWMS ServiceType = "WMS" + ServiceTypeWFS ServiceType = "WFS" +) + +// HorizontalPodAutoscalerPatch - copy of autoscalingv2.HorizontalPodAutoscalerSpec without ScaleTargetRef +// This way we don't have to specify the scaleTargetRef field in the CRD. +type HorizontalPodAutoscalerPatch struct { + MinReplicas *int32 `json:"minReplicas,omitempty"` + MaxReplicas *int32 `json:"maxReplicas,omitempty"` + Metrics []autoscalingv2.MetricSpec `json:"metrics,omitempty"` + Behavior *autoscalingv2.HorizontalPodAutoscalerBehavior `json:"behavior,omitempty"` +} + +// WMSWFS is the common interface used for both WMS and WFS resources. +// +kubebuilder:object:generate=false +type WMSWFS interface { + *WFS | *WMS + metav1.Object + + GroupKind() schema.GroupKind + Inspire() *WFSInspire + Mapfile() *Mapfile + PodSpecPatch() corev1.PodSpec + HorizontalPodAutoscalerPatch() *HorizontalPodAutoscalerPatch + Type() ServiceType + TypedName() string + Options() Options + HasPostgisData() bool + OwnerInfoRef() string + + // URL returns the configured service URL + URL() smoothoperatormodel.URL + IngressRouteURLs(includeServiceURLWhenEmpty bool) smoothoperatormodel.IngressRouteURLs + + // DatasetMetadataIds returns list of all configured metadata identifiers configured on Layers or Featuretypes + DatasetMetadataIDs() []string + + GeoPackages() []*Gpkg + + ReadinessQueryString() (string, string, error) +} + +// Mapfile references a ConfigMap key where an external mapfile is stored. +// +kubebuilder:validation:Type=object +type Mapfile struct { + // +kubebuilder:validation:Type=object + ConfigMapKeyRef corev1.ConfigMapKeySelector `json:"configMapKeyRef"` +} + +// BaseOptions for all apis +type BaseOptions struct { + // IncludeIngress dictates whether to deploy an Ingress or ensure none exists. + // +kubebuilder:default:=true + // +kubebuilder:validation:Optional + IncludeIngress bool `json:"includeIngress"` + + // AutomaticCasing enables automatic conversion from snake_case to camelCase. + // +kubebuilder:default:=true + // +kubebuilder:validation:Optional + AutomaticCasing bool `json:"automaticCasing"` + + // Whether to prefetch data from blob storage, and store it on the local filesystem. + // If `false`, the data will be served directly out of blob storage + // +kubebuilder:default:=true + // +kubebuilder:validation:Optional + PrefetchData bool `json:"prefetchData"` +} + +// Options configures optional behaviors of the operator, like ingress, casing, and data prefetching. +// +kubebuilder:validation:Type=object +type Options struct { + BaseOptions `json:",inline"` + WMSOptions `json:",inline"` +} + +func GetDefaultOptions() *Options { + return &Options{ + BaseOptions: BaseOptions{ + IncludeIngress: true, + AutomaticCasing: true, + PrefetchData: true, + }, + WMSOptions: WMSOptions{ + ValidateRequests: true, + RewriteGroupToDataLayers: false, + DisableWebserviceProxy: false, + ValidateChildStyleNameEqual: false, + }, + } +} + +// BaseService holds all shared Services field for all apis +type BaseService struct { + // Geonovum subdomein + // +kubebuilder:validation:MinLength:=1 + Prefix string `json:"prefix"` + + // URL of the service + URL smoothoperatormodel.URL `json:"url"` + + // External Mapfile reference + Mapfile *Mapfile `json:"mapfile,omitempty"` + + // Reference to OwnerInfo CR + // +kubebuilder:validation:MinLength:=1 + OwnerInfoRef string `json:"ownerInfoRef"` + + // Service title + // +kubebuilder:validation:MinLength:=1 + Title string `json:"title"` + + // Service abstract + // +kubebuilder:validation:MinLength:=1 + Abstract string `json:"abstract"` + + // Keywords for capabilities + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:items:MinLength:=1 + Keywords []string `json:"keywords"` + + // Optional Fees + // +kubebuilder:validation:MinLength:=1 + Fees *string `json:"fees,omitempty"` + + // AccessConstraints URL + // +kubebuilder:default="https://creativecommons.org/publicdomain/zero/1.0/deed.nl" + AccessConstraints smoothoperatormodel.URL `json:"accessConstraints,omitempty"` +} + +// Inspire holds INSPIRE-specific metadata for the service. +// +kubebuilder:validation:Type=object +type Inspire struct { + // ServiceMetadataURL references the CSW or custom metadata record. + // +kubebuilder:validation:Type=object + ServiceMetadataURL MetadataURL `json:"serviceMetadataUrl"` + + // Language of the INSPIRE metadata record + // +kubebuilder:validation:Pattern:=`bul|cze|dan|dut|eng|est|fin|fre|ger|gre|hun|gle|ita|lav|lit|mlt|pol|por|rum|slo|slv|spa|swe` + Language string `json:"language"` +} + +// +kubebuilder:validation:XValidation:rule="(has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom))", message="metadataUrl should have exactly 1 of csw or custom" +type MetadataURL struct { + // CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. + CSW *Metadata `json:"csw,omitempty"` + + // Custom allows arbitrary href + Custom *Custom `json:"custom,omitempty"` +} + +// Metadata holds the UUID of a CSW metadata record +type Metadata struct { + // MetadataIdentifier is the record's UUID + // +kubebuilder:validation:Pattern:=`^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$` + MetadataIdentifier string `json:"metadataIdentifier"` +} + +// Custom represents a non-CSW metadata link with a href and MIME type. +// +kubebuilder:validation:Type=object +type Custom struct { + // Href of the custom metadata url + Href smoothoperatormodel.URL `json:"href"` + + // MIME type of the custom link + // +kubebuilder:validation:MinLength=1 + Type string `json:"type"` +} + +// BaseData holds the data source configuration for gpkg and postgis +type BaseData struct { + // Gpkg configures a GeoPackage file source + Gpkg *Gpkg `json:"gpkg,omitempty"` + + // Postgis configures a Postgis table source + Postgis *Postgis `json:"postgis,omitempty"` +} + +// Data holds the data source configuration +// +kubebuilder:validation:XValidation:rule="has(self.gpkg) || has(self.tif) || has(self.postgis)", message="Atleast one of the datasource should be provided (postgis, gpkg, tif)" +type Data struct { + BaseData `json:",inline"` + + // TIF configures a GeoTIF raster source + TIF *TIF `json:"tif,omitempty"` +} + +// Gpkg configures a Geopackage data source +// +kubebuilder:validation:Type=object +type Gpkg struct { + // Blobkey identifies the location/bucket of the .gpkg file + // +kubebuilder:validation:Pattern:=^.+\/.+\/.+\.gpkg$ + BlobKey string `json:"blobKey"` + + // TableName is the table within the geopackage + // +kubebuilder:validation:MinLength:=1 + TableName string `json:"tableName"` + + // GeometryType of the table, must match an OGC type + // +kubebuilder:validation:Pattern:=`^(Multi)?(Point|LineString|Polygon)$` + GeometryType string `json:"geometryType"` + + // Columns to visualize for this table + // +kubebuilder:validation:MinItems:=1 + Columns []Column `json:"columns"` +} + +// Postgis - reference to table in a Postgres database +// +kubebuilder:validation:Type=object +type Postgis struct { + // TableName in postGIS + // +kubebuilder:validation:MinLength=1 + TableName string `json:"tableName"` + + // GeometryType of the table + // +kubebuilder:validation:Pattern=`^(Multi)?(Point|LineString|Polygon)$` + GeometryType string `json:"geometryType"` + + // Columns to expose from table + // +kubebuilder:validation:MinItems=1 + Columns []Column `json:"columns"` +} + +// TIF configures a GeoTIFF raster data source +// +kubebuilder:validation:Type=object +type TIF struct { + // BlobKey to the TIFF file + // +kubebuilder:validation:Pattern:=`^.+\/.+\/.+\.(tif?f|vrt)$` + BlobKey string `json:"blobKey"` + + // This option can be used to control the resampling kernel used sampling raster images, optional + // +kubebuilder:validation:Pattern=`(NEAREST|AVERAGE|BILINEAR)` + // +kubebuilder:default=NEAREST + Resample string `json:"resample,omitempty"` + + // Controls the smoothing of the image on a certain point. Bigger value gives a smoother/better picture but + // results in slower web responses, optional + // +kubebuilder:validation:Pattern="^-?[0-9]+([.][0-9]*)?$" + // +kubebuilder:default="2.5" + OversampleRatio string `json:"oversampleRatio,omitempty"` + + // Sets the color index to treat as transparent for raster layers, optional, hex or rgb + // +kubebuilder:validation:Pattern=`(#[0-9A-F]{6}([0-9A-F]{2})?)|([0-9]{1,3}\s[0-9]{1,3}\s[0-9]{1,3})` + Offsite *string `json:"offsite,omitempty"` + + // "When a band represents nominal or ordinal data the class name (from styling) can be included in the getFeatureInfo" + // +kubebuilder:default:=false + GetFeatureInfoIncludesClass bool `json:"getFeatureInfoIncludesClass,omitempty"` +} + +// Column maps a source column name to an optional alias for output. +// +kubebuilder:validation:Type=object +type Column struct { + // Name of the column in the data source. + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + + // Alias for the column in the service output. + // +kubebuilder:validation:MinLength=1 + Alias *string `json:"alias,omitempty"` +} + +func SetHost(url string) { + host = strings.TrimSuffix(url, "/") +} + +func GetHost(includeProtocol bool) string { + if !includeProtocol && strings.HasPrefix(host, "http") { + return strings.Split(host, "://")[1] + } + + return host +} + +func (d *BaseData) GetColumns() *[]Column { + switch { + case d.Gpkg != nil: + return &d.Gpkg.Columns + case d.Postgis != nil: + return &d.Postgis.Columns + default: + return nil + } +} + +func (d *BaseData) GetTableName() *string { + switch { + case d.Gpkg != nil: + return &d.Gpkg.TableName + case d.Postgis != nil: + return &d.Postgis.TableName + default: + return nil + } +} + +func (d *BaseData) GetGeometryType() *string { + switch { + case d.Gpkg != nil: + return &d.Gpkg.GeometryType + case d.Postgis != nil: + return &d.Postgis.GeometryType + default: + return nil + } +} + +func (o Options) UseWebserviceProxy() bool { + // options.DisableWebserviceProxy not set or false + return !o.DisableWebserviceProxy +} diff --git a/api/v3/shared_validation.go b/api/v3/shared_validation.go new file mode 100644 index 0000000..bd87685 --- /dev/null +++ b/api/v3/shared_validation.go @@ -0,0 +1,210 @@ +package v3 + +import ( + "context" + "fmt" + "slices" + + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + sharedValidation "github.com/pdok/smooth-operator/pkg/validation" + v1 "k8s.io/api/core/v1" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateCreate[W WMSWFS](c client.Client, obj W, validate func(W, *[]string, *field.ErrorList)) ([]string, error) { + warnings := []string{} + allErrs := field.ErrorList{} + + err := sharedValidation.ValidateLabelsOnCreate(obj.GetLabels()) + if err != nil { + allErrs = append(allErrs, err) + } + + err = sharedValidation.ValidateIngressRouteURLsContainsBaseURL(obj.IngressRouteURLs(false), obj.URL(), nil) + if err != nil { + allErrs = append(allErrs, err) + } + + validate(obj, &warnings, &allErrs) + ValidateOwnerInfo(c, obj, &allErrs) + + if len(allErrs) == 0 { + return warnings, nil + } + + return warnings, apierrors.NewInvalid( + obj.GroupKind(), + obj.GetName(), allErrs) +} + +func ValidateUpdate[W WMSWFS](c client.Client, newW, oldW W, validate func(W, *[]string, *field.ErrorList)) ([]string, error) { + warnings := []string{} + allErrs := field.ErrorList{} + + // Make sure no ingressRouteURLs have been removed + sharedValidation.ValidateIngressRouteURLsNotRemoved(oldW.IngressRouteURLs(false), newW.IngressRouteURLs(true), &allErrs, nil) + + if len(newW.IngressRouteURLs(false)) == 0 { + // There are no ingressRouteURLs given, spec.service.url is immutable is that case. + path := field.NewPath("spec").Child("service").Child("url") + sharedValidation.CheckURLImmutability( + oldW.URL(), + newW.URL(), + &allErrs, + path, + ) + } else if oldW.URL().String() != newW.URL().String() { + // Make sure both the old spec.service.url and the new one are included in the ingressRouteURLs list. + err := sharedValidation.ValidateIngressRouteURLsContainsBaseURL(newW.IngressRouteURLs(true), oldW.URL(), nil) + if err != nil { + allErrs = append(allErrs, err) + } + + err = sharedValidation.ValidateIngressRouteURLsContainsBaseURL(newW.IngressRouteURLs(true), newW.URL(), nil) + if err != nil { + allErrs = append(allErrs, err) + } + } + + sharedValidation.ValidateLabelsOnUpdate(oldW.GetLabels(), newW.GetLabels(), &allErrs) + + if (newW.Inspire() == nil && oldW.Inspire() != nil) || (newW.Inspire() != nil && oldW.Inspire() == nil) { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec").Child("service").Child("inspire"), "cannot change from inspire to not inspire or the other way around")) + } + + validate(newW, &warnings, &allErrs) + ValidateOwnerInfo(c, newW, &allErrs) + + if len(allErrs) == 0 { + return warnings, nil + } + return warnings, apierrors.NewInvalid( + newW.GroupKind(), + newW.GetName(), allErrs) +} + +func ValidateHorizontalPodAutoscalerPatch(patch HorizontalPodAutoscalerPatch, allErrs *field.ErrorList) { + path := field.NewPath("spec").Child("horizontalPodAutoscaler") + // TODO: replace hardcoded defaults with dynamic defaults from cli options or ownerInfo + var minReplicas, maxReplicas int32 = 2, 32 + if patch.MinReplicas != nil { + minReplicas = *patch.MinReplicas + } + if patch.MaxReplicas != nil { + maxReplicas = *patch.MaxReplicas + } + + if maxReplicas < minReplicas { + replicas := fmt.Sprintf("minReplicas: %d, maxReplicas: %d", minReplicas, maxReplicas) + + *allErrs = append(*allErrs, field.Invalid(path, replicas, "maxReplicas cannot be less than minReplicas")) + } + +} + +func ValidateEphemeralStorage(podSpecPatch v1.PodSpec, allErrs *field.ErrorList) { + path := field.NewPath("spec"). + Child("podSpecPatch"). + Child("containers"). + Key("mapserver"). + Child("resources"). + Child("limits"). + Child(v1.ResourceEphemeralStorage.String()) + storageSet := false + for _, container := range podSpecPatch.Containers { + if container.Name == "mapserver" { + _, storageSet = container.Resources.Limits[v1.ResourceEphemeralStorage] + } + } + if !storageSet { + *allErrs = append(*allErrs, field.Required(path, "")) + } +} + +func ValidateInspire[O WMSWFS](obj O, allErrs *field.ErrorList, allWarnings *[]string) { + if obj.Inspire() == nil { + return + } + + datasetIDs := obj.DatasetMetadataIDs() + spatialID := obj.Inspire().SpatialDatasetIdentifier + + if slices.Contains(datasetIDs, spatialID) { + *allWarnings = append(*allWarnings, field.Invalid( + field.NewPath("spec").Child("service").Child("inspire").Child("spatialDatasetIdentifier"), + spatialID, + "spatialDatasetIdentifier should not also be used as an datasetMetadataUrl.csw.metadataIdentifier", + ).Error()) + } + + if serviceID := obj.Inspire().ServiceMetadataURL.CSW; serviceID != nil { + if slices.Contains(datasetIDs, serviceID.MetadataIdentifier) { + *allErrs = append(*allErrs, field.Invalid( + field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), + serviceID.MetadataIdentifier, + "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as an datasetMetadataUrl.csw.metadataIdentifier", + )) + } + + if spatialID == serviceID.MetadataIdentifier { + *allErrs = append(*allErrs, field.Invalid( + field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), + serviceID.MetadataIdentifier, + "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as the spatialDatasetIdentifier", + )) + } + } + + if obj.Type() == ServiceTypeWFS && len(datasetIDs) > 1 { + *allErrs = append(*allErrs, field.Invalid( + field.NewPath("spec").Child("service").Child("featureTypes[*]").Child("datasetMetadataUrl").Child("csw").Child("metadataIdentifier"), + datasetIDs, + "when Inspire, all featureTypes need use the same datasetMetadataUrl.csw.metadataIdentifier", + )) + } + +} + +func ValidateOwnerInfo[O WMSWFS](c client.Client, obj O, allErrs *field.ErrorList) { + ownerInfoRef := obj.OwnerInfoRef() + ownerInfo := &smoothoperatorv1.OwnerInfo{} + objectKey := client.ObjectKey{ + Namespace: obj.GetNamespace(), + Name: ownerInfoRef, + } + ctx := context.Background() + err := c.Get(ctx, objectKey, ownerInfo) + fieldPath := field.NewPath("spec").Child("service").Child("ownerInfoRef") + if err != nil { + *allErrs = append(*allErrs, field.NotFound(fieldPath, ownerInfoRef)) + return + } + + if ownerInfo.Spec.NamespaceTemplate == nil { + *allErrs = append(*allErrs, field.Required(fieldPath, "spec.namespaceTemplate missing in "+ownerInfo.Name)) + return + } + + if ((obj.Inspire() != nil && obj.Inspire().ServiceMetadataURL.CSW != nil) || + len(obj.DatasetMetadataIDs()) > 0) && + (ownerInfo.Spec.MetadataUrls == nil || ownerInfo.Spec.MetadataUrls.CSW == nil) { + *allErrs = append(*allErrs, field.Required(fieldPath, "spec.metadataUrls.csw missing in "+ownerInfo.Name)) + return + } + + switch obj.Type() { + case ServiceTypeWFS: + if ownerInfo.Spec.WFS == nil { + *allErrs = append(*allErrs, field.Required(fieldPath, "spec.WFS missing in "+ownerInfo.Name)) + } + case ServiceTypeWMS: + if ownerInfo.Spec.WMS == nil { + *allErrs = append(*allErrs, field.Required(fieldPath, "spec.WMS missing in "+ownerInfo.Name)) + } + } + +} diff --git a/api/v3/wfs_conversion.go b/api/v3/wfs_conversion.go index 4e46996..372bdb3 100644 --- a/api/v3/wfs_conversion.go +++ b/api/v3/wfs_conversion.go @@ -1,17 +1,25 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v3 diff --git a/api/v3/wfs_types.go b/api/v3/wfs_types.go index 85e8121..3a67839 100644 --- a/api/v3/wfs_types.go +++ b/api/v3/wfs_types.go @@ -1,55 +1,66 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v3 import ( + "errors" + "slices" + "strings" + + smoothoperatormodel "github.com/pdok/smooth-operator/model" + + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -// WFSSpec defines the desired state of WFS. -type WFSSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of WFS. Edit wfs_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// WFSStatus defines the observed state of WFS. -type WFSStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:conversion:hub // +kubebuilder:subresource:status +// versionName=v3 +// +kubebuilder:resource:path=wfs +// +kubebuilder:resource:categories=pdok +// +kubebuilder:printcolumn:name="ReadyPods",type=integer,JSONPath=`.status.podSummary[0].ready` +// +kubebuilder:printcolumn:name="DesiredPods",type=integer,JSONPath=`.status.podSummary[0].total` +// +kubebuilder:printcolumn:name="ReconcileStatus",type=string,JSONPath=`.status.conditions[?(@.type == "Reconciled")].reason` // WFS is the Schema for the wfs API. type WFS struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec WFSSpec `json:"spec,omitempty"` - Status WFSStatus `json:"status,omitempty"` + Spec WFSSpec `json:"spec"` + Status smoothoperatormodel.OperatorStatus `json:"status,omitempty"` +} + +func (wfs *WFS) OperatorStatus() *smoothoperatormodel.OperatorStatus { + return &wfs.Status } // +kubebuilder:object:root=true @@ -64,3 +75,247 @@ type WFSList struct { func init() { SchemeBuilder.Register(&WFS{}, &WFSList{}) } + +// WFSSpec vertegenwoordigt de hoofdstruct voor de YAML-configuratie +// +kubebuilder:validation:XValidation:rule="!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)",messageExpression="'ingressRouteUrls should include service.url '+self.service.url" +type WFSSpec struct { + // Optional lifecycle settings + Lifecycle *smoothoperatormodel.Lifecycle `json:"lifecycle,omitempty"` + + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. + PodSpecPatch corev1.PodSpec `json:"podSpecPatch"` + HorizontalPodAutoscalerPatch *HorizontalPodAutoscalerPatch `json:"horizontalPodAutoscalerPatch,omitempty"` + // TODO omitting the options field or setting an empty value results in incorrect defaulting of the options + // Options configures optional behaviors of the operator, like ingress, casing, and data prefetching. + Options *BaseOptions `json:"options,omitempty"` + + // Custom healthcheck options + HealthCheck *HealthCheckWFS `json:"healthCheck,omitempty"` + + // Optional list of URLs where the service can be reached + // By default only the spec.service.url is used + IngressRouteURLs smoothoperatormodel.IngressRouteURLs `json:"ingressRouteUrls,omitempty"` + + // service configuration + Service WFSService `json:"service"` +} + +// +kubebuilder:validation:XValidation:message="otherCrs can't contain the defaultCrs",rule="!has(self.otherCrs) || (has(self.otherCrs) && !(self.defaultCrs in self.otherCrs))",fieldPath=".otherCrs" +type WFSService struct { + BaseService `json:",inline"` + + // Inspire holds INSPIRE-specific metadata for the service. + Inspire *WFSInspire `json:"inspire,omitempty"` + + // Default CRS (DataEPSG) + // +kubebuilder:validation:Pattern:="^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$" + DefaultCrs string `json:"defaultCrs"` + + // Other supported CRS + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:items:Pattern:="^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$" + OtherCrs []string `json:"otherCrs,omitempty"` + + // Service bounding box + Bbox *Bbox `json:"bbox,omitempty"` + + // CountDefault -> wfs_maxfeatures in mapfile + // +kubebuilder:validation:Minimum:=1 + CountDefault *int `json:"countDefault,omitempty"` + + // FeatureTypes configurations + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:Type=array + FeatureTypes []FeatureType `json:"featureTypes"` +} + +func (s WFSService) KeywordsIncludingInspireKeyword() []string { + keywords := s.Keywords + if s.Inspire != nil && !slices.Contains(keywords, "infoFeatureAccessService") { + keywords = append(keywords, "infoFeatureAccessService") + } + + return keywords +} + +// HealthCheck is the struct with all fields to configure custom healthchecks +type HealthCheckWFS struct { + // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('service=wfs')",message="a valid healthcheck contains 'Service=WFS'" + // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('request=')",message="a valid healthcheck contains 'Request='" + Querystring string `json:"querystring"` + // +kubebuilder:validation:Pattern=(image/png|text/xml|text/html) + Mimetype string `json:"mimetype"` +} + +type WFSInspire struct { + Inspire `json:",inline"` + // SpatialDatasetIdentifier is the ID uniquely identifying the dataset. + // +kubebuilder:validation:Pattern:=`^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$` + SpatialDatasetIdentifier string `json:"spatialDatasetIdentifier"` +} + +type Bbox struct { + // EXTENT/wfs_extent in mapfile + //nolint:tagliatelle + // +kubebuilder:validation:Type=object + DefaultCRS smoothoperatormodel.BBox `json:"defaultCRS"` +} + +// FeatureType defines a WFS feature +type FeatureType struct { + // Name of the feature + // +kubebuilder:validation:Pattern:=`^\S+$` + Name string `json:"name"` + + // Title of the feature + // +kubebuilder:validation:MinLength:=1 + Title string `json:"title"` + + // Abstract of the feature + // +kubebuilder:validation:MinLength:=1 + Abstract string `json:"abstract"` + + // Keywords of the feature + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:items:MinLength:=1 + Keywords []string `json:"keywords"` + + // Metadata URL + // +kubebuilder:validation:Type=object + DatasetMetadataURL *MetadataURL `json:"datasetMetadataUrl,omitempty"` + + // Optional feature bbox + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=object + Bbox *FeatureBbox `json:"bbox,omitempty"` + + // FeatureType data connection + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:XValidation:rule="has(self.gpkg) || has(self.postgis)", message="At least one of the datasource should be provided (postgis, gpkg)" + Data BaseData `json:"data"` +} + +// FeatureBbox is the optional featureType bounding box, if provided it overrides the default extent +type FeatureBbox struct { + // DefaultCRS defines the EXTENT/wfs_extent for the featureType for use in the mapfile + //nolint:tagliatelle + // +kubebuilder:validation:Type=object + DefaultCRS *smoothoperatormodel.BBox `json:"defaultCRS,omitempty"` + + // WGS84, if provided, gives the same bounding box reprojected into EPSG:4326 for use in the capabilities. + // +kubebuilder:validation:Type=object + WGS84 *smoothoperatormodel.BBox `json:"wgs84,omitempty"` +} + +func (wfs *WFS) HasPostgisData() bool { + for _, featureType := range wfs.Spec.Service.FeatureTypes { + if featureType.Data.Postgis != nil { + return true + } + } + return false +} + +func (wfs *WFS) GroupKind() schema.GroupKind { + return schema.GroupKind{Group: GroupVersion.Group, Kind: wfs.Kind} +} + +func (wfs *WFS) Inspire() *WFSInspire { + return wfs.Spec.Service.Inspire +} + +func (wfs *WFS) Mapfile() *Mapfile { + return wfs.Spec.Service.Mapfile +} + +func (wfs *WFS) Type() ServiceType { + return ServiceTypeWFS +} + +func (wfs *WFS) TypedName() string { + name := wfs.GetName() + typeSuffix := strings.ToLower(string(ServiceTypeWFS)) + + if strings.HasSuffix(name, typeSuffix) { + return name + } + + return name + "-" + typeSuffix +} + +func (wfs *WFS) PodSpecPatch() corev1.PodSpec { + return wfs.Spec.PodSpecPatch +} + +func (wfs *WFS) HorizontalPodAutoscalerPatch() *HorizontalPodAutoscalerPatch { + return wfs.Spec.HorizontalPodAutoscalerPatch +} + +func (wfs *WFS) Options() Options { + if wfs.Spec.Options == nil { + return *GetDefaultOptions() + } + + return Options{BaseOptions: *wfs.Spec.Options} +} + +func (wfs *WFS) URL() smoothoperatormodel.URL { + return wfs.Spec.Service.URL +} + +func (wfs *WFS) DatasetMetadataIDs() []string { + ids := []string{} + + for _, featureType := range wfs.Spec.Service.FeatureTypes { + if featureType.DatasetMetadataURL != nil && featureType.DatasetMetadataURL.CSW != nil { + if id := featureType.DatasetMetadataURL.CSW.MetadataIdentifier; !slices.Contains(ids, id) { + ids = append(ids, id) + } + } + } + + return ids +} + +func (wfs *WFS) GeoPackages() []*Gpkg { + gpkgs := make([]*Gpkg, 0) + + for _, ft := range wfs.Spec.Service.FeatureTypes { + if ft.Data.Gpkg != nil { + gpkgs = append(gpkgs, ft.Data.Gpkg) + } + } + + return gpkgs +} + +func (wfs *WFS) ReadinessQueryString() (string, string, error) { + if hc := wfs.Spec.HealthCheck; hc != nil { + return hc.Querystring, hc.Mimetype, nil + } + + if len(wfs.Spec.Service.FeatureTypes) == 0 { + return "", "", errors.New("cannot get readiness probe for WFS, featuretypes could not be found") + } + + return "SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=" + wfs.Spec.Service.FeatureTypes[0].Name + "&STARTINDEX=0&COUNT=1", "text/xml", nil +} + +func (wfs *WFS) IngressRouteURLs(includeServiceURLWhenEmpty bool) smoothoperatormodel.IngressRouteURLs { + if len(wfs.Spec.IngressRouteURLs) == 0 { + if includeServiceURLWhenEmpty { + return smoothoperatormodel.IngressRouteURLs{{URL: wfs.Spec.Service.URL}} + } + + return smoothoperatormodel.IngressRouteURLs{} + } + + return wfs.Spec.IngressRouteURLs +} + +func (wfs *WFS) OwnerInfoRef() string { + return wfs.Spec.Service.OwnerInfoRef +} diff --git a/api/v3/wfs_validation.go b/api/v3/wfs_validation.go new file mode 100644 index 0000000..62eee7a --- /dev/null +++ b/api/v3/wfs_validation.go @@ -0,0 +1,101 @@ +package v3 + +import ( + "slices" + "strings" + + "sigs.k8s.io/controller-runtime/pkg/client" + + sharedValidation "github.com/pdok/smooth-operator/pkg/validation" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func (wfs *WFS) ValidateCreate(c client.Client) ([]string, error) { + return ValidateCreate(c, wfs, ValidateWFS) +} + +func (wfs *WFS) ValidateUpdate(c client.Client, wfsOld *WFS) ([]string, error) { + return ValidateUpdate(c, wfs, wfsOld, ValidateWFS) +} + +func ValidateWFS(wfs *WFS, warnings *[]string, allErrs *field.ErrorList) { + if strings.Contains(wfs.GetName(), "wfs") { + sharedValidation.AddWarning( + warnings, + *field.NewPath("metadata").Child("name"), + "name should not contain wfs", + wfs.GroupVersionKind(), + wfs.GetName(), + ) + } + + service := wfs.Spec.Service + path := field.NewPath("spec").Child("service") + + if service.Mapfile == nil && service.DefaultCrs != "EPSG:28992" && service.Bbox == nil { + *allErrs = append(*allErrs, field.Required( + path.Child("bbox").Child("defaultCRS"), + "when service.defaultCRS is not 'EPSG:28992'", + )) + } + + if service.Mapfile != nil && service.Bbox != nil { + sharedValidation.AddWarning( + warnings, + *path.Child("bbox"), + "is not used when service.mapfile is configured", + wfs.GroupVersionKind(), + wfs.GetName(), + ) + } + + crsses := []string{} + for i, crs := range service.OtherCrs { + if slices.Contains(crsses, crs) { + *allErrs = append(*allErrs, field.Duplicate( + path.Child("otherCrs").Index(i), + crs, + )) + } else { + crsses = append(crsses, crs) + } + } + + ValidateInspire(wfs, allErrs, warnings) + + if wfs.Spec.HorizontalPodAutoscalerPatch != nil { + ValidateHorizontalPodAutoscalerPatch(*wfs.Spec.HorizontalPodAutoscalerPatch, allErrs) + } + + podSpecPatch := wfs.Spec.PodSpecPatch + ValidateEphemeralStorage(podSpecPatch, allErrs) + + ValidateFeatureTypes(wfs, warnings, allErrs) +} + +func ValidateFeatureTypes(wfs *WFS, warnings *[]string, allErrs *field.ErrorList) { + names := []string{} + path := field.NewPath("spec").Child("service").Child("featureTypes") + for index, featureType := range wfs.Spec.Service.FeatureTypes { + if slices.Contains(names, featureType.Name) { + *allErrs = append(*allErrs, field.Duplicate( + path.Index(index).Child("name"), + featureType.Name, + )) + } else { + names = append(names, featureType.Name) + } + + if wfs.Spec.Service.Mapfile != nil && featureType.Bbox != nil && featureType.Bbox.DefaultCRS != nil { + sharedValidation.AddWarning( + warnings, + *path.Index(index).Child("bbox").Child("defaultCrs"), + "is not used when service.mapfile is configured", + wfs.GroupVersionKind(), + wfs.GetName(), + ) + } + + } +} diff --git a/api/v3/wms_conversion.go b/api/v3/wms_conversion.go index 2ff6ed1..c262d39 100644 --- a/api/v3/wms_conversion.go +++ b/api/v3/wms_conversion.go @@ -1,17 +1,25 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v3 diff --git a/api/v3/wms_types.go b/api/v3/wms_types.go index c00588e..e0d9f79 100644 --- a/api/v3/wms_types.go +++ b/api/v3/wms_types.go @@ -1,55 +1,75 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v3 import ( + "errors" + "fmt" + "maps" + "slices" + "sort" + "strings" + + smoothoperatormodel "github.com/pdok/smooth-operator/model" + + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + TopLayer = "topLayer" + DataLayer = "dataLayer" + GroupLayer = "groupLayer" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. -// WMSSpec defines the desired state of WMS. -type WMSSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of WMS. Edit wms_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// WMSStatus defines the observed state of WMS. -type WMSStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:conversion:hub // +kubebuilder:subresource:status +// versionName=v3 +// +kubebuilder:resource:path=wms +// +kubebuilder:resource:categories=pdok +// +kubebuilder:printcolumn:name="ReadyPods",type=integer,JSONPath=`.status.podSummary[0].ready` +// +kubebuilder:printcolumn:name="DesiredPods",type=integer,JSONPath=`.status.podSummary[0].total` +// +kubebuilder:printcolumn:name="ReconcileStatus",type=string,JSONPath=`.status.conditions[?(@.type == "Reconciled")].reason` // WMS is the Schema for the wms API. type WMS struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec WMSSpec `json:"spec,omitempty"` - Status WMSStatus `json:"status,omitempty"` + Spec WMSSpec `json:"spec"` + Status smoothoperatormodel.OperatorStatus `json:"status,omitempty"` +} + +func (wms *WMS) OperatorStatus() *smoothoperatormodel.OperatorStatus { + return &wms.Status } // +kubebuilder:object:root=true @@ -64,3 +84,626 @@ type WMSList struct { func init() { SchemeBuilder.Register(&WMS{}, &WMSList{}) } + +// WMSSpec defines the desired state of WMS. +// +kubebuilder:validation:XValidation:rule="!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)",messageExpression="'ingressRouteUrls should include service.url '+self.service.url" +type WMSSpec struct { + // Optional lifecycle settings + Lifecycle *smoothoperatormodel.Lifecycle `json:"lifecycle,omitempty"` + + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. + PodSpecPatch corev1.PodSpec `json:"podSpecPatch"` + + // Optional specification for the HorizontalAutoscaler + HorizontalPodAutoscalerPatch *HorizontalPodAutoscalerPatch `json:"horizontalPodAutoscalerPatch,omitempty"` + + // Optional options for the configuration of the service. + // TODO omitting the options field or setting an empty value results in incorrect defaulting of the options + Options *Options `json:"options,omitempty"` + + // Custom healthcheck options + HealthCheck *HealthCheckWMS `json:"healthCheck,omitempty"` + + // Optional list of URLs where the service can be reached + // By default only the spec.service.url is used + IngressRouteURLs smoothoperatormodel.IngressRouteURLs `json:"ingressRouteUrls,omitempty"` + + // Service specification + Service WMSService `json:"service"` +} + +// +kubebuilder:validation:XValidation:message="service requires styling, either through service.mapfile, or stylingAssets.configMapRefs",rule=has(self.mapfile) || (has(self.stylingAssets) && has(self.stylingAssets.configMapRefs)) +// +kubebuilder:validation:XValidation:message="when using service.mapfile, don't include stylingAssets.configMapRefs",rule=!has(self.mapfile) || (!has(self.stylingAssets) || !has(self.stylingAssets.configMapRefs)) +type WMSService struct { + BaseService `json:",inline"` + + // Config for Inspire services + Inspire *Inspire `json:"inspire,omitempty"` + + // CRS of the data + // +kubebuilder:validation:Pattern:=`(EPSG|CRS):\d+` + //nolint:tagliatelle + DataEPSG string `json:"dataEPSG"` + + // Mapfile setting: Sets the maximum size (in pixels) for both dimensions of the image from a getMap request. + // +kubebuilder:validation:Minimum:=1 + MaxSize *int32 `json:"maxSize,omitempty"` + + // Mapfile setting: Sets the RESOLUTION field in the mapfile, not used when service.mapfile is configured + Resolution *int32 `json:"resolution,omitempty"` + + // Mapfile setting: Sets the DEFRESOLUTION field in the mapfile, not used when service.mapfile is configured + DefResolution *int32 `json:"defResolution,omitempty"` + + // Optional. Required files for the styling of the service + StylingAssets *StylingAssets `json:"stylingAssets,omitempty"` + + // Custom mapfile + Mapfile *Mapfile `json:"mapfile,omitempty"` + + // Toplayer + Layer Layer `json:"layer"` +} + +func (wmsService WMSService) KeywordsIncludingInspireKeyword() []string { + keywords := wmsService.Keywords + if wmsService.Inspire != nil && !slices.Contains(keywords, "infoMapAccessService") { + keywords = append(keywords, "infoMapAccessService") + } + + return keywords +} + +// HealthCheck is the struct with all fields to configure custom healthchecks +// +kubebuilder:validation:XValidation:rule="!has(self.querystring) || has(self.mimetype)",message="mimetype is required when a querystring is used" +// +kubebuilder:validation:XValidation:rule="(has(self.boundingbox) || has(self.querystring)) && !(has(self.querystring) && has(self.boundingbox))", message="healthcheck should have exactly 1 of querystring + mimetype or boundingbox" +// +kubebuilder:validation:XValidation:rule="(has(self.boundingbox) || has(self.mimetype)) && !(has(self.mimetype) && has(self.boundingbox))", message="healthcheck should have exactly 1 of querystring + mimetype or boundingbox" +type HealthCheckWMS struct { + // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('service=wms')",message="a valid healthcheck contains 'SERVICE=WMS'" + // +kubebuilder:validation:XValidation:rule="self.lowerAscii().contains('request=')",message="a valid healthcheck contains 'REQUEST='" + Querystring *string `json:"querystring,omitempty"` + // +kubebuilder:validation:Pattern=(image/png|text/xml|text/html) + Mimetype *string `json:"mimetype,omitempty"` + + Boundingbox *smoothoperatormodel.BBox `json:"boundingbox,omitempty"` +} + +// StylingAssets contains the files references needed for styling +// +kubebuilder:validation:XValidation:message="At least one of blobKeys or configMapRefs is required",rule="has(self.blobKeys) || has(self.configMapRefs)" +type StylingAssets struct { + // BlobKeys contains symbol image (.png/.svg) or font (.ttf) keys on blob storage, format: container/key/file.(png|ttf) + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:items:Pattern:=^.+\/.+\/.+\.(png|ttf|svg)$ + BlobKeys []string `json:"blobKeys,omitempty"` + + // +kubebuilder:validation:MinItems:=1 + ConfigMapRefs []ConfigMapRef `json:"configMapRefs,omitempty"` +} + +type ConfigMapRef struct { + // Name is the name of the ConfigMap + // +kubebuilder:validation:MinLength:=1 + Name string `json:"name"` + + // Keys contains styling assets that contain mapfile code (.style|.symbol), required if you use symbols in your styles + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:items:Pattern:=^\S*.\.(style|symbol) + Keys []string `json:"keys,omitempty"` +} + +// +kubebuilder:validation:XValidation:message="A layer should have exactly one of sublayers or data", rule="(has(self.data) || has(self.layers)) && !(has(self.data) && has(self.layers))" +// +kubebuilder:validation:XValidation:message="A layer with data attribute should have styling", rule="!has(self.data) || has(self.styles)" +// +kubebuilder:validation:XValidation:message="A layer should have a title when visible", rule="!self.visible || has(self.title)" +// +kubebuilder:validation:XValidation:message="A layer should have an abstract when visible", rule="!self.visible || has(self.abstract)" +// +kubebuilder:validation:XValidation:message="A layer should have keywords when visible", rule="!self.visible || has(self.keywords)" +type Layer struct { + // Name of the layer, required for layers on the 2nd or 3rd level + // +kubebuilder:validation:MinLength:=1 + Name *string `json:"name,omitempty"` + + // Title of the layer + // +kubebuilder:validation:MinLength:=1 + Title *string `json:"title,omitempty"` + + // Abstract of the layer + // +kubebuilder:validation:MinLength:=1 + Abstract *string `json:"abstract,omitempty"` + + // Keywords of the layer, required if the layer is visible + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:items:MinLength:=1 + Keywords []string `json:"keywords,omitempty"` + + // BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. + // +kubebuilder:validation:MinItems:=1 + BoundingBoxes []WMSBoundingBox `json:"boundingBoxes,omitempty"` + + // Whether or not the layer is visible. At least one of the layers must be visible. + // +kubebuilder:default:=true + // +kubebuilder:validation:Optional + Visible bool `json:"visible"` + + // TODO ?? + Authority *Authority `json:"authority,omitempty"` + + // Links to metadata + DatasetMetadataURL *MetadataURL `json:"datasetMetadataUrl,omitempty"` + + // The minimum scale at which this layer functions + // +kubebuilder:validation:Pattern:=`^[0-9]+(.[0-9]+)?$` + MinScaleDenominator *string `json:"minscaledenominator,omitempty"` + + // The maximum scale at which this layer functions + // +kubebuilder:validation:Pattern:=`^[1-9][0-9]*(.[0-9]+)?$` + MaxScaleDenominator *string `json:"maxscaledenominator,omitempty"` + + // List of styles used by the layer + // +kubebuilder:validation:MinItems:=1 + Styles []Style `json:"styles,omitempty"` + + // Mapfile setting, sets "LABEL_NO_CLIP=ON" + LabelNoClip bool `json:"labelNoClip,omitempty"` + + // Data (gpkg/postgis/tif) used by the layer + Data *Data `json:"data,omitempty"` + + // Sublayers of the layer + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:Type=array + Layers []Layer `json:"layers,omitempty"` +} + +type WMSBoundingBox struct { + // +kubebuilder:validation:Pattern:="^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$" + CRS string `json:"crs"` + BBox smoothoperatormodel.BBox `json:"bbox"` +} + +func (wmsBoundingBox *WMSBoundingBox) ToExtent() string { + return wmsBoundingBox.BBox.ToExtent() +} + +func (wmsBoundingBox *WMSBoundingBox) Combine(other *WMSBoundingBox) { + if wmsBoundingBox.CRS != other.CRS { + return + } + wmsBoundingBox.BBox.Combine(other.BBox) +} + +type Authority struct { + Name string `json:"name"` + URL string `json:"url"` + SpatialDatasetIdentifier string `json:"spatialDatasetIdentifier"` +} + +type Style struct { + // +kubebuilder:validation:MinLength:=1 + Name string `json:"name"` + + // +kubebuilder:validation:MinLength:=1 + Title *string `json:"title,omitempty"` + + // +kubebuilder:validation:MinLength:=1 + Abstract *string `json:"abstract,omitempty"` + + // +kubebuilder:validation:MinLength:=1 + Visualization *string `json:"visualization,omitempty"` + + Legend *Legend `json:"legend,omitempty"` +} + +type Legend struct { + // The width of the legend in px, defaults to 78 + // + kubebuilder:default=78 + Width int32 `json:"width,omitempty"` + + // The height of the legend in px, defaults to 20 + // + kubebuilder:default=20 + Height int32 `json:"height,omitempty"` + + // Format of the legend, defaults to image/png + // +kubebuilder:default="image/png" + Format string `json:"format,omitempty"` + + // Location of the legend on the blobstore + // +kubebuilder:validation:MinLength:=1 + BlobKey string `json:"blobKey"` +} + +// WMSOptions are the Options exclusively used by the WMS +// +kubebuilder:validation:Type=object +type WMSOptions struct { + + // ValidateRequests enables request validation against the service schema. + // +kubebuilder:default:=true + // +kubebuilder:validation:Optional + ValidateRequests bool `json:"validateRequests"` + + // RewriteGroupToDataLayers merges group layers into individual data layers. + // +kubebuilder:default:=false + // +kubebuilder:validation:Optional + RewriteGroupToDataLayers bool `json:"rewriteGroupToDataLayers"` + + // DisableWebserviceProxy disables the built-in proxy for external web services. + // +kubebuilder:default:=false + // +kubebuilder:validation:Optional + DisableWebserviceProxy bool `json:"disableWebserviceProxy"` + + // ValidateChildStyleNameEqual ensures child style names match the parent style. + // +kubebuilder:default=false + // +kubebuilder:validation:Optional + ValidateChildStyleNameEqual bool `json:"validateChildStyleNameEqual"` +} + +func (wmsService *WMSService) GetBoundingBox() WMSBoundingBox { + var boundingBox *WMSBoundingBox + + allLayers := wmsService.GetAnnotatedLayers() + for _, layer := range allLayers { + if len(layer.BoundingBoxes) > 0 { + for _, bbox := range wmsService.Layer.BoundingBoxes { + if boundingBox == nil { + boundingBox = &bbox + } else { + boundingBox.Combine(&bbox) + } + } + } + } + + if boundingBox != nil { + return *boundingBox + } + + return WMSBoundingBox{ + CRS: "EPSG:28992", + BBox: smoothoperatormodel.BBox{ + MinX: "-25000", + MaxX: "280000", + MinY: "250000", + MaxY: "860000", + }, + } +} + +func (stylingAssets *StylingAssets) GetAllConfigMapRefKeys() []string { + keys := []string{} + if stylingAssets != nil { + for _, cmRef := range stylingAssets.ConfigMapRefs { + keys = append(keys, cmRef.Keys...) + } + } + return keys +} + +type AnnotatedLayer struct { + // The name of the group that this layer belongs to, nil if it is not a member of a group. Groups can be a member of the toplayer as a group + GroupName *string + // Only for spec.Service.Layer + IsTopLayer bool + // Top layer or layer below the toplayer with children itself + IsGroupLayer bool + // Contains actual data + IsDataLayer bool + Layer +} + +func (wmsService *WMSService) GetAnnotatedLayers() []AnnotatedLayer { + result := make([]AnnotatedLayer, 0) + + result = append(result, AnnotatedLayer{ + GroupName: nil, + IsTopLayer: true, + IsGroupLayer: true, + IsDataLayer: false, + Layer: wmsService.Layer, + }) + + for _, middleLayer := range wmsService.Layer.Layers { + result = append(result, AnnotatedLayer{ + GroupName: wmsService.Layer.Name, + IsTopLayer: false, + IsGroupLayer: middleLayer.IsGroupLayer(), + IsDataLayer: middleLayer.IsDataLayer(), + Layer: middleLayer, + }) + + for _, bottomLayer := range middleLayer.Layers { + result = append(result, AnnotatedLayer{ + GroupName: middleLayer.Name, + IsTopLayer: false, + IsGroupLayer: false, + IsDataLayer: true, + Layer: bottomLayer, + }) + } + } + + return result +} + +// GetAllSublayers - get all sublayers of a layer, the result does not include the layer itself +func (layer *Layer) GetAllSublayers() []Layer { + layers := layer.Layers + for _, childLayer := range layer.Layers { + layers = append(layers, childLayer.GetAllSublayers()...) + } + return layers +} + +func (wmsService *WMSService) GetParentLayer(layer Layer) *Layer { + if wmsService.Layer.Layers == nil { + return nil + } + + for _, middleLayer := range wmsService.Layer.Layers { + if middleLayer.Name == layer.Name { + return &wmsService.Layer + } + + for _, bottomLayer := range middleLayer.Layers { + if bottomLayer.Name == layer.Name { + return &middleLayer + } + } + } + return nil +} + +func (layer *Layer) hasData() bool { + switch { + case layer.Data == nil: + return false + case layer.Data.Gpkg != nil: + return true + case layer.Data.Postgis != nil: + return true + case layer.Data.TIF != nil: + return true + default: + return false + } +} + +func (layer *Layer) hasTIFData() bool { + if !layer.hasData() { + return false + } + return layer.Data.TIF != nil && layer.Data.TIF.BlobKey != "" +} + +func (layer *Layer) IsDataLayer() bool { + return layer.hasData() && len(layer.Layers) == 0 +} + +func (layer *Layer) IsGroupLayer() bool { + return len(layer.Layers) > 0 +} + +// IsTopLayer - a layer is a toplayer if and only if it has sublayers that are group layers. +// In other words the layer is level 1 in a 3 level hierarchy. +func (layer *Layer) IsTopLayer() bool { + if layer.IsGroupLayer() { + for _, childLayer := range layer.Layers { + if childLayer.IsGroupLayer() { + return true + } + } + } + + return false +} + +func (layer *Layer) hasBoundingBoxForCRS(crs string) bool { + for _, bbox := range layer.BoundingBoxes { + if bbox.CRS == crs { + return true + } + } + return false +} + +func (layer *Layer) setInheritedBoundingBoxes() { + if len(layer.Layers) == 0 { + return + } + + var updatedLayers []Layer + for _, childLayer := range layer.Layers { + // Inherit parent boundingboxes + for _, boundingBox := range layer.BoundingBoxes { + if !childLayer.hasBoundingBoxForCRS(boundingBox.CRS) { + childLayer.BoundingBoxes = append(childLayer.BoundingBoxes, boundingBox) + } + } + childLayer.setInheritedBoundingBoxes() + updatedLayers = append(updatedLayers, childLayer) + } + layer.Layers = updatedLayers +} + +func (wms *WMS) GetAllLayersWithLegend() (layers []AnnotatedLayer) { + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if !layer.hasData() || len(layer.Styles) == 0 { + continue + } + for _, style := range layer.Styles { + if style.Legend != nil && style.Legend.BlobKey != "" { + layers = append(layers, layer) + break + } + } + } + return +} + +func (wms *WMS) GetUniqueTiffBlobKeys() []string { + blobKeys := map[string]bool{} + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if layer.hasTIFData() { + blobKeys[layer.Data.TIF.BlobKey] = true + } + } + keys := slices.Collect(maps.Keys(blobKeys)) + sort.Strings(keys) // This is only needed for the unit test + return keys +} + +func (wms *WMS) GetAuthority() *Authority { + if wms.Spec.Service.Layer.Authority != nil { + return wms.Spec.Service.Layer.Authority + } + + for _, childLayer := range wms.Spec.Service.Layer.Layers { + if childLayer.Authority != nil { + return childLayer.Authority + } else if childLayer.Layers != nil { + for _, grandChildLayer := range childLayer.Layers { + if grandChildLayer.Authority != nil { + return grandChildLayer.Authority + } + } + } + } + + return nil +} + +func (wms *WMS) HasPostgisData() bool { + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if layer.Data != nil && layer.Data.Postgis != nil { + return true + } + } + return false +} + +func (wms *WMS) GroupKind() schema.GroupKind { + return schema.GroupKind{Group: GroupVersion.Group, Kind: wms.Kind} +} + +func (wms *WMS) Inspire() *WFSInspire { + if wms.Spec.Service.Inspire != nil { + return &WFSInspire{Inspire: *wms.Spec.Service.Inspire} + } + return nil +} + +func (wms *WMS) Mapfile() *Mapfile { + return wms.Spec.Service.Mapfile +} + +func (wms *WMS) Type() ServiceType { + return ServiceTypeWMS +} + +func (wms *WMS) TypedName() string { + name := wms.GetName() + typeSuffix := strings.ToLower(string(ServiceTypeWMS)) + + if strings.HasSuffix(name, typeSuffix) { + return name + } + + return name + "-" + typeSuffix +} + +func (wms *WMS) PodSpecPatch() corev1.PodSpec { + return wms.Spec.PodSpecPatch +} + +func (wms *WMS) HorizontalPodAutoscalerPatch() *HorizontalPodAutoscalerPatch { + return wms.Spec.HorizontalPodAutoscalerPatch +} + +func (wms *WMS) Options() Options { + if wms.Spec.Options == nil { + return *GetDefaultOptions() + } + + return *wms.Spec.Options +} + +func (wms *WMS) URL() smoothoperatormodel.URL { + return wms.Spec.Service.URL +} + +func (wms *WMS) DatasetMetadataIDs() []string { + ids := []string{} + + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if layer.DatasetMetadataURL != nil && layer.DatasetMetadataURL.CSW != nil { + if id := layer.DatasetMetadataURL.CSW.MetadataIdentifier; !slices.Contains(ids, id) { + ids = append(ids, id) + } + } + } + + return ids +} + +func (wms *WMS) GeoPackages() []*Gpkg { + gpkgs := make([]*Gpkg, 0) + + for _, layer := range wms.Spec.Service.Layer.Layers { + if layer.Data != nil { + if layer.Data.Gpkg != nil { + gpkgs = append(gpkgs, layer.Data.Gpkg) + } + } else if layer.Layers != nil { + for _, childLayer := range layer.Layers { + if childLayer.Data != nil && childLayer.Data.Gpkg != nil { + gpkgs = append(gpkgs, childLayer.Data.Gpkg) + } + } + } + } + + return gpkgs +} + +func (wms *WMS) HealthCheckBBox() string { + if hc := wms.Spec.HealthCheck; hc != nil && hc.Boundingbox != nil { + return strings.ReplaceAll(hc.Boundingbox.ToExtent(), " ", ",") + } + + return "190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914" +} + +func (wms *WMS) ReadinessQueryString() (string, string, error) { + if hc := wms.Spec.HealthCheck; hc != nil && hc.Querystring != nil { + return *hc.Querystring, *hc.Mimetype, nil + } + + firstDataLayerName := "" + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if layer.IsDataLayer { + firstDataLayerName = *layer.Name + break + } + } + if firstDataLayerName == "" { + return "", "", errors.New("cannot get readiness probe for WMS, the first datalayer could not be found") + } + + return fmt.Sprintf("SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=%s&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=%s&STYLES=&FORMAT=image/png", wms.HealthCheckBBox(), firstDataLayerName), "image/png", nil +} + +func (wms *WMS) IngressRouteURLs(includeServiceURLWhenEmpty bool) smoothoperatormodel.IngressRouteURLs { + if len(wms.Spec.IngressRouteURLs) == 0 { + if includeServiceURLWhenEmpty { + return smoothoperatormodel.IngressRouteURLs{{URL: wms.Spec.Service.URL}} + } + + return smoothoperatormodel.IngressRouteURLs{} + } + + return wms.Spec.IngressRouteURLs +} + +func (wms *WMS) OwnerInfoRef() string { + return wms.Spec.Service.OwnerInfoRef +} diff --git a/api/v3/wms_types_test.go b/api/v3/wms_types_test.go new file mode 100644 index 0000000..9e328c6 --- /dev/null +++ b/api/v3/wms_types_test.go @@ -0,0 +1,166 @@ +package v3 + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + + smoothoperatormodel "github.com/pdok/smooth-operator/model" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +func TestLayer_setInheritedBoundingBoxes(t *testing.T) { + first28992BoundingBox := WMSBoundingBox{ + CRS: "EPSG:28992", + BBox: smoothoperatormodel.BBox{ + MinX: "482.06", + MaxX: "306602.42", + MinY: "284182.97", + MaxY: "637049.52", + }, + } + first4326BoundingBox := WMSBoundingBox{ + CRS: "EPSG:4326", + BBox: smoothoperatormodel.BBox{ + MinX: "2.35417303", + MaxX: "7.5553525", + MinY: "50.71447164", + MaxY: "55.66948102", + }, + } + first4258BoundingBox := WMSBoundingBox{ + CRS: "EPSG:4258", + BBox: smoothoperatormodel.BBox{ + MinX: "2.354173", + MaxX: "7.5553527", + MinY: "50.71447", + MaxY: "55.66948", + }} + second28992BoundingBox := WMSBoundingBox{ + CRS: "EPSG:28992", + BBox: smoothoperatormodel.BBox{ + MinX: "0.00", + MaxX: "310000.00", + MinY: "275000.00", + MaxY: "650000.00", + }} + + tests := []struct { + name string + layer Layer + toplayerExpectedBoundingBoxCount int + toplayerExpectedBoundingBoxes []WMSBoundingBox + grouplayer1ExpectedBoundingBoxCount int + grouplayer1ExpectedBoundingBoxes []WMSBoundingBox + datalayer1ExpectedBoundingBoxCount int + datalayer1ExpectedBoundingBoxes []WMSBoundingBox + datalayer2ExpectedBoundingBoxCount int + datalayer2ExpectedBoundingBoxes []WMSBoundingBox + }{ + { + name: "setInheritedBoundingBoxes for layer", + layer: Layer{ + Name: smoothoperatorutils.Pointer("toplayer"), + BoundingBoxes: []WMSBoundingBox{first28992BoundingBox}, + Layers: []Layer{ + { + Name: smoothoperatorutils.Pointer("grouplayer-1"), + BoundingBoxes: []WMSBoundingBox{first4326BoundingBox}, + Layers: []Layer{ + { + Name: smoothoperatorutils.Pointer("datalayer-1"), + BoundingBoxes: []WMSBoundingBox{first4258BoundingBox}, + }, + { + Name: smoothoperatorutils.Pointer("datalayer-2"), + BoundingBoxes: []WMSBoundingBox{second28992BoundingBox}, + }, + }, + }, + }, + }, + toplayerExpectedBoundingBoxCount: 1, + toplayerExpectedBoundingBoxes: []WMSBoundingBox{first28992BoundingBox}, + grouplayer1ExpectedBoundingBoxCount: 2, + grouplayer1ExpectedBoundingBoxes: []WMSBoundingBox{first4326BoundingBox, first28992BoundingBox}, + datalayer1ExpectedBoundingBoxCount: 3, + datalayer1ExpectedBoundingBoxes: []WMSBoundingBox{first4258BoundingBox, first4326BoundingBox, first28992BoundingBox}, + datalayer2ExpectedBoundingBoxCount: 2, + datalayer2ExpectedBoundingBoxes: []WMSBoundingBox{second28992BoundingBox, first4326BoundingBox}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + layer := tt.layer + layer.setInheritedBoundingBoxes() + + topChildLayers := layer.Layers + groupLayer1 := topChildLayers[0] + groupChildLayers := groupLayer1.Layers + dataLayer1 := groupChildLayers[0] + dataLayer2 := groupChildLayers[1] + + if len(layer.BoundingBoxes) != tt.toplayerExpectedBoundingBoxCount { + t.Errorf("Toplayer has unexpected number of bounding boxes = %v, want %v", len(layer.BoundingBoxes), tt.toplayerExpectedBoundingBoxCount) + } + if !cmp.Equal(layer.BoundingBoxes, tt.toplayerExpectedBoundingBoxes) { + t.Errorf("Toplayer has unexpected bounding boxes = %v, want %v", layer.BoundingBoxes, tt.toplayerExpectedBoundingBoxes) + } + if len(groupLayer1.BoundingBoxes) != tt.grouplayer1ExpectedBoundingBoxCount { + t.Errorf("Grouplayer has unexpected number of bounding boxes = %v, want %v", len(groupLayer1.BoundingBoxes), tt.grouplayer1ExpectedBoundingBoxCount) + } + if !cmp.Equal(groupLayer1.BoundingBoxes, tt.grouplayer1ExpectedBoundingBoxes) { + t.Errorf("Grouplayer has unexpected bounding boxes = %v, want %v", groupLayer1.BoundingBoxes, tt.grouplayer1ExpectedBoundingBoxes) + } + if len(dataLayer1.BoundingBoxes) != tt.datalayer1ExpectedBoundingBoxCount { + t.Errorf("Datalayer1 has unexpected number of bounding boxes = %v, want %v", len(dataLayer1.BoundingBoxes), tt.datalayer1ExpectedBoundingBoxCount) + } + if !cmp.Equal(dataLayer1.BoundingBoxes, tt.datalayer1ExpectedBoundingBoxes) { + t.Errorf("Datalayer1 has unexpected bounding boxes = %v, want %v", dataLayer1.BoundingBoxes, tt.datalayer1ExpectedBoundingBoxes) + } + if len(dataLayer2.BoundingBoxes) != tt.datalayer2ExpectedBoundingBoxCount { + t.Errorf("Datalayer2 has unexpected number of bounding boxes = %v, want %v", len(dataLayer2.BoundingBoxes), tt.datalayer2ExpectedBoundingBoxCount) + } + if !cmp.Equal(dataLayer2.BoundingBoxes, tt.datalayer2ExpectedBoundingBoxes) { + t.Errorf("Datalayer2 has unexpected bounding boxes = %v, want %v", dataLayer2.BoundingBoxes, tt.datalayer2ExpectedBoundingBoxes) + } + }) + } +} + +func TestLayer_GetParent(t *testing.T) { + childLayer2 := Layer{Name: smoothoperatorutils.Pointer("childlayer-2")} + childLayer1 := Layer{Name: smoothoperatorutils.Pointer("childlayer-1"), Layers: []Layer{childLayer2}} + topLayer := Layer{Name: smoothoperatorutils.Pointer("toplayer"), Layers: []Layer{childLayer1}} + + type args struct { + service WMSService + } + tests := []struct { + name string + layer Layer + args args + want *Layer + }{ + { + name: "Test GetParent on layer with parent", + layer: childLayer2, + args: args{service: WMSService{Layer: topLayer}}, + want: &childLayer1, + }, + { + name: "Test GetParent on layer without parent", + layer: topLayer, + args: args{service: WMSService{Layer: topLayer}}, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.args.service.GetParentLayer(tt.layer); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetParent() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/api/v3/wms_validation.go b/api/v3/wms_validation.go new file mode 100644 index 0000000..ae02f98 --- /dev/null +++ b/api/v3/wms_validation.go @@ -0,0 +1,334 @@ +package v3 + +import ( + "fmt" + "strings" + + "sigs.k8s.io/controller-runtime/pkg/client" + + sharedValidation "github.com/pdok/smooth-operator/pkg/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/strings/slices" +) + +func (wms *WMS) ValidateCreate(c client.Client) ([]string, error) { + return ValidateCreate(c, wms, ValidateWMS) +} + +func (wms *WMS) ValidateUpdate(c client.Client, wmsOld *WMS) ([]string, error) { + return ValidateUpdate(c, wms, wmsOld, ValidateWMS) +} + +func ValidateWMS(wms *WMS, warnings *[]string, allErrs *field.ErrorList) { + if strings.Contains(wms.GetName(), "wms") { + sharedValidation.AddWarning( + warnings, + *field.NewPath("metadata").Child("name"), + "name should not contain wms", + wms.GroupVersionKind(), + wms.GetName(), + ) + } + + if wms.Mapfile() != nil { + service := wms.Spec.Service + path := field.NewPath("spec").Child("service") + if service.Resolution != nil { + sharedValidation.AddWarning( + warnings, + *path.Child("resolution"), + "not used when service.mapfile is configured", + wms.GroupVersionKind(), + wms.GetName(), + ) + } + if service.DefResolution != nil { + sharedValidation.AddWarning( + warnings, + *path.Child("defResolution"), + "not used when service.mapfile is configured", + wms.GroupVersionKind(), + wms.GetName(), + ) + } + } + + ValidateInspire(wms, allErrs, warnings) + if wms.HorizontalPodAutoscalerPatch() != nil { + ValidateHorizontalPodAutoscalerPatch(*wms.HorizontalPodAutoscalerPatch(), allErrs) + } + ValidateEphemeralStorage(wms.PodSpecPatch(), allErrs) + + validateLayers(wms, warnings, allErrs) +} + +func validateLayers(wms *WMS, warnings *[]string, allErrs *field.ErrorList) { + + layerNames := []string{} + hasVisibleLayer := false + + topLayer := AnnotatedLayer{ + GroupName: nil, + IsTopLayer: true, + IsGroupLayer: true, + IsDataLayer: false, + Layer: wms.Spec.Service.Layer, + } + + validateLayer(topLayer, field.NewPath("spec").Child("service").Child("layer"), []string{}, &layerNames, &hasVisibleLayer, wms, warnings, allErrs) + + if !hasVisibleLayer { + *allErrs = append(*allErrs, field.Required( + field.NewPath("spec").Child("service").Child("layer").Child("layers[*]").Child("visible"), + "at least one layer must be visible", + )) + } +} + +func validateLayer(layer AnnotatedLayer, path *field.Path, groupStyles []string, layerNames *[]string, hasVisibleLayer *bool, wms *WMS, warnings *[]string, allErrs *field.ErrorList) { + service := wms.Spec.Service + + var layerName string + if layer.IsTopLayer && layer.Name == nil { + layerName = "unnamed: " + TopLayer + } else { + layerName = *layer.Name + } + + if slices.Contains(*layerNames, layerName) { + *allErrs = append(*allErrs, field.Duplicate( + path.Child("name"), + layerName, + )) + } else { + *layerNames = append(*layerNames, layerName) + } + + if layer.IsGroupLayer && layer.Data != nil { + *allErrs = append(*allErrs, field.Invalid( + path.Child("data"), + layer.Data, + "must not be set on a GroupLayer", + )) + } + + validateLayerWithMapfile(layer, path, wms, warnings, allErrs) + + if layer.Visible { + if !layer.IsTopLayer { + *hasVisibleLayer = true + } + } else { + validateNotVisibleLayer(layer, path, wms, warnings, allErrs) + } + + crsses := []string{} + for i, bbox := range layer.BoundingBoxes { + if slices.Contains(crsses, bbox.CRS) { + *allErrs = append(*allErrs, field.Duplicate( + path.Child("boundingBoxes").Index(i).Child("crs"), + bbox.CRS, + )) + } else { + crsses = append(crsses, bbox.CRS) + } + } + + styleNames := []string{} + for i, style := range layer.Styles { + stylePath := path.Child("styles").Index(i) + validateStyle(style, stylePath, &styleNames, &groupStyles, service.StylingAssets.GetAllConfigMapRefKeys(), layer, service.Mapfile != nil, allErrs) + } + + if layer.IsDataLayer { + for _, groupStyle := range groupStyles { + if !slices.Contains(styleNames, groupStyle) { + *allErrs = append(*allErrs, field.Invalid( + path.Child("styles"), + nil, + fmt.Sprintf("dataLayer must implement style: %s, defined by a parent layer", groupStyle), + )) + } + } + } + + for i, subLayer := range layer.Layers { + annotatedSubLayer := AnnotatedLayer{ + GroupName: layer.Name, + IsTopLayer: false, + IsGroupLayer: subLayer.IsGroupLayer(), + IsDataLayer: subLayer.IsDataLayer(), + Layer: subLayer, + } + validateLayer(annotatedSubLayer, path.Child("layers").Index(i), groupStyles, layerNames, hasVisibleLayer, wms, warnings, allErrs) + } + +} + +func validateLayerWithMapfile(layer AnnotatedLayer, path *field.Path, wms *WMS, warnings *[]string, allErrs *field.ErrorList) { + service := wms.Spec.Service + hasCustomMapfile := service.Mapfile != nil + if hasCustomMapfile && layer.BoundingBoxes != nil { + sharedValidation.AddWarning( + warnings, + *path.Child("boundingBoxes"), + "is not used when service.mapfile is configured", + wms.GroupVersionKind(), + wms.GetName(), + ) + } + if !hasCustomMapfile && service.DataEPSG != "EPSG:28992" && !layer.hasBoundingBoxForCRS(service.DataEPSG) && layer.Name != nil { + *allErrs = append(*allErrs, field.Required( + path.Child("boundingBoxes").Child("crs"), + fmt.Sprintf("must contain a boundingBox for CRS %s when service.dataEPSG is not 'EPSG:28992'", service.DataEPSG), + )) + } + + if layer.IsDataLayer && hasCustomMapfile { + if tif := layer.Data.TIF; tif != nil { + tifWarnings(tif, path, wms, warnings) + } + } + +} + +func tifWarnings(tif *TIF, path *field.Path, wms *WMS, warnings *[]string) { + if tif.Resample != "NEAREST" { + sharedValidation.AddWarning( + warnings, + *path.Child("data").Child("tif").Child("resample"), + "is not used when service.mapfile is configured", + wms.GroupVersionKind(), + wms.GetName(), + ) + } + + if tif.Offsite != nil { + sharedValidation.AddWarning( + warnings, + *path.Child("data").Child("tif").Child("offsite"), + "is not used when service.mapfile is configured", + wms.GroupVersionKind(), + wms.GetName(), + ) + } + + if tif.GetFeatureInfoIncludesClass { + sharedValidation.AddWarning( + warnings, + *path.Child("data").Child("tif").Child("getFeatureInfoIncludesClass"), + "is not used when service.mapfile is configured", + wms.GroupVersionKind(), + wms.GetName(), + ) + } +} + +func validateStyle(style Style, path *field.Path, styleNames *[]string, groupStyles *[]string, stylingFiles []string, layer AnnotatedLayer, usesCustomMapfile bool, allErrs *field.ErrorList) { + if slices.Contains(*styleNames, style.Name) { + *allErrs = append(*allErrs, field.Invalid( + path.Child("name"), + style.Name, + "A Layer can't use the same style name multiple times", + )) + } else { + *styleNames = append(*styleNames, style.Name) + } + + if layer.Visible && !slices.Contains(*groupStyles, style.Name) && style.Title == nil { + *allErrs = append(*allErrs, field.Required( + path.Child("title"), + "A Style must have a title on the highest visible Layer", + )) + } + + if layer.IsGroupLayer { + if slices.Contains(*groupStyles, style.Name) { + *allErrs = append(*allErrs, field.Invalid( + path.Child("name"), + style.Name, + "A GroupLayer can't redefine the same style as a parent layer", + )) + } else { + *groupStyles = append(*groupStyles, style.Name) + } + + if style.Visualization != nil { + *allErrs = append(*allErrs, field.Invalid( + path.Child("visualization"), + style.Visualization, + "GroupLayers must not have a visualization", + )) + } + } + + if layer.IsDataLayer { + switch { + case usesCustomMapfile && style.Visualization != nil: + *allErrs = append(*allErrs, field.Invalid( + path.Child("visualization"), + style.Visualization, + "is not used when spec.service.mapfile is used", + )) + case !usesCustomMapfile && style.Visualization == nil: + *allErrs = append(*allErrs, field.Required( + path.Child("visualization"), + "on DataLayers when spec.service.mapfile is not used", + )) + case !usesCustomMapfile && !slices.Contains(stylingFiles, *style.Visualization): + *allErrs = append(*allErrs, field.Invalid( + path.Child("visualization"), + style.Visualization, + "must be defined be in spec.service.stylingAssets.configMapKeyRefs.Keys", + )) + } + + } +} + +func validateNotVisibleLayer(layer AnnotatedLayer, path *field.Path, wms *WMS, warnings *[]string, allErrs *field.ErrorList) { + if layer.IsGroupLayer { + *allErrs = append(*allErrs, field.Invalid( + path.Child("visible"), + layer.Visible, + "must be true for a "+GroupLayer, + )) + } + paths := []field.Path{} + + if layer.Title != nil { + paths = append(paths, *path.Child("title")) + } + if layer.Abstract != nil { + paths = append(paths, *path.Child("abstract")) + } + if layer.Keywords != nil { + paths = append(paths, *path.Child("keywords")) + } + if layer.DatasetMetadataURL != nil { + paths = append(paths, *path.Child("datasetMetadataURL")) + } + if layer.Authority != nil { + paths = append(paths, *path.Child("authority")) + } + + for i, style := range layer.Styles { + if style.Title != nil { + paths = append(paths, *path.Child("styles").Index(i).Child("title")) + } + if style.Abstract != nil { + paths = append(paths, *path.Child("styles").Index(i).Child("abstract")) + } + } + + for _, path := range paths { + sharedValidation.AddWarning( + warnings, + path, + "is not used when layer.visible=false", + wms.GroupVersionKind(), + wms.GetName(), + ) + } + +} diff --git a/api/v3/zz_generated.deepcopy.go b/api/v3/zz_generated.deepcopy.go index e2bdc59..137e8a6 100644 --- a/api/v3/zz_generated.deepcopy.go +++ b/api/v3/zz_generated.deepcopy.go @@ -1,19 +1,27 @@ //go:build !ignore_autogenerated /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ // Code generated by controller-gen. DO NOT EDIT. @@ -21,16 +29,666 @@ limitations under the License. package v3 import ( + "github.com/pdok/smooth-operator/model" + "k8s.io/api/autoscaling/v2" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnnotatedLayer) DeepCopyInto(out *AnnotatedLayer) { + *out = *in + if in.GroupName != nil { + in, out := &in.GroupName, &out.GroupName + *out = new(string) + **out = **in + } + in.Layer.DeepCopyInto(&out.Layer) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnnotatedLayer. +func (in *AnnotatedLayer) DeepCopy() *AnnotatedLayer { + if in == nil { + return nil + } + out := new(AnnotatedLayer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authority) DeepCopyInto(out *Authority) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authority. +func (in *Authority) DeepCopy() *Authority { + if in == nil { + return nil + } + out := new(Authority) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BaseData) DeepCopyInto(out *BaseData) { + *out = *in + if in.Gpkg != nil { + in, out := &in.Gpkg, &out.Gpkg + *out = new(Gpkg) + (*in).DeepCopyInto(*out) + } + if in.Postgis != nil { + in, out := &in.Postgis, &out.Postgis + *out = new(Postgis) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseData. +func (in *BaseData) DeepCopy() *BaseData { + if in == nil { + return nil + } + out := new(BaseData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BaseOptions) DeepCopyInto(out *BaseOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseOptions. +func (in *BaseOptions) DeepCopy() *BaseOptions { + if in == nil { + return nil + } + out := new(BaseOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BaseService) DeepCopyInto(out *BaseService) { + *out = *in + in.URL.DeepCopyInto(&out.URL) + if in.Mapfile != nil { + in, out := &in.Mapfile, &out.Mapfile + *out = new(Mapfile) + (*in).DeepCopyInto(*out) + } + if in.Keywords != nil { + in, out := &in.Keywords, &out.Keywords + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Fees != nil { + in, out := &in.Fees, &out.Fees + *out = new(string) + **out = **in + } + in.AccessConstraints.DeepCopyInto(&out.AccessConstraints) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseService. +func (in *BaseService) DeepCopy() *BaseService { + if in == nil { + return nil + } + out := new(BaseService) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Bbox) DeepCopyInto(out *Bbox) { + *out = *in + out.DefaultCRS = in.DefaultCRS +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bbox. +func (in *Bbox) DeepCopy() *Bbox { + if in == nil { + return nil + } + out := new(Bbox) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Column) DeepCopyInto(out *Column) { + *out = *in + if in.Alias != nil { + in, out := &in.Alias, &out.Alias + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Column. +func (in *Column) DeepCopy() *Column { + if in == nil { + return nil + } + out := new(Column) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMapRef) DeepCopyInto(out *ConfigMapRef) { + *out = *in + if in.Keys != nil { + in, out := &in.Keys, &out.Keys + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapRef. +func (in *ConfigMapRef) DeepCopy() *ConfigMapRef { + if in == nil { + return nil + } + out := new(ConfigMapRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Custom) DeepCopyInto(out *Custom) { + *out = *in + in.Href.DeepCopyInto(&out.Href) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Custom. +func (in *Custom) DeepCopy() *Custom { + if in == nil { + return nil + } + out := new(Custom) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Data) DeepCopyInto(out *Data) { + *out = *in + in.BaseData.DeepCopyInto(&out.BaseData) + if in.TIF != nil { + in, out := &in.TIF, &out.TIF + *out = new(TIF) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Data. +func (in *Data) DeepCopy() *Data { + if in == nil { + return nil + } + out := new(Data) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureBbox) DeepCopyInto(out *FeatureBbox) { + *out = *in + if in.DefaultCRS != nil { + in, out := &in.DefaultCRS, &out.DefaultCRS + *out = new(model.BBox) + **out = **in + } + if in.WGS84 != nil { + in, out := &in.WGS84, &out.WGS84 + *out = new(model.BBox) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureBbox. +func (in *FeatureBbox) DeepCopy() *FeatureBbox { + if in == nil { + return nil + } + out := new(FeatureBbox) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeatureType) DeepCopyInto(out *FeatureType) { + *out = *in + if in.Keywords != nil { + in, out := &in.Keywords, &out.Keywords + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DatasetMetadataURL != nil { + in, out := &in.DatasetMetadataURL, &out.DatasetMetadataURL + *out = new(MetadataURL) + (*in).DeepCopyInto(*out) + } + if in.Bbox != nil { + in, out := &in.Bbox, &out.Bbox + *out = new(FeatureBbox) + (*in).DeepCopyInto(*out) + } + in.Data.DeepCopyInto(&out.Data) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureType. +func (in *FeatureType) DeepCopy() *FeatureType { + if in == nil { + return nil + } + out := new(FeatureType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Gpkg) DeepCopyInto(out *Gpkg) { + *out = *in + if in.Columns != nil { + in, out := &in.Columns, &out.Columns + *out = make([]Column, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Gpkg. +func (in *Gpkg) DeepCopy() *Gpkg { + if in == nil { + return nil + } + out := new(Gpkg) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthCheckWFS) DeepCopyInto(out *HealthCheckWFS) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckWFS. +func (in *HealthCheckWFS) DeepCopy() *HealthCheckWFS { + if in == nil { + return nil + } + out := new(HealthCheckWFS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthCheckWMS) DeepCopyInto(out *HealthCheckWMS) { + *out = *in + if in.Querystring != nil { + in, out := &in.Querystring, &out.Querystring + *out = new(string) + **out = **in + } + if in.Mimetype != nil { + in, out := &in.Mimetype, &out.Mimetype + *out = new(string) + **out = **in + } + if in.Boundingbox != nil { + in, out := &in.Boundingbox, &out.Boundingbox + *out = new(model.BBox) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckWMS. +func (in *HealthCheckWMS) DeepCopy() *HealthCheckWMS { + if in == nil { + return nil + } + out := new(HealthCheckWMS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HorizontalPodAutoscalerPatch) DeepCopyInto(out *HorizontalPodAutoscalerPatch) { + *out = *in + if in.MinReplicas != nil { + in, out := &in.MinReplicas, &out.MinReplicas + *out = new(int32) + **out = **in + } + if in.MaxReplicas != nil { + in, out := &in.MaxReplicas, &out.MaxReplicas + *out = new(int32) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make([]v2.MetricSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Behavior != nil { + in, out := &in.Behavior, &out.Behavior + *out = new(v2.HorizontalPodAutoscalerBehavior) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalPodAutoscalerPatch. +func (in *HorizontalPodAutoscalerPatch) DeepCopy() *HorizontalPodAutoscalerPatch { + if in == nil { + return nil + } + out := new(HorizontalPodAutoscalerPatch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Inspire) DeepCopyInto(out *Inspire) { + *out = *in + in.ServiceMetadataURL.DeepCopyInto(&out.ServiceMetadataURL) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Inspire. +func (in *Inspire) DeepCopy() *Inspire { + if in == nil { + return nil + } + out := new(Inspire) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Layer) DeepCopyInto(out *Layer) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Title != nil { + in, out := &in.Title, &out.Title + *out = new(string) + **out = **in + } + if in.Abstract != nil { + in, out := &in.Abstract, &out.Abstract + *out = new(string) + **out = **in + } + if in.Keywords != nil { + in, out := &in.Keywords, &out.Keywords + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.BoundingBoxes != nil { + in, out := &in.BoundingBoxes, &out.BoundingBoxes + *out = make([]WMSBoundingBox, len(*in)) + copy(*out, *in) + } + if in.Authority != nil { + in, out := &in.Authority, &out.Authority + *out = new(Authority) + **out = **in + } + if in.DatasetMetadataURL != nil { + in, out := &in.DatasetMetadataURL, &out.DatasetMetadataURL + *out = new(MetadataURL) + (*in).DeepCopyInto(*out) + } + if in.MinScaleDenominator != nil { + in, out := &in.MinScaleDenominator, &out.MinScaleDenominator + *out = new(string) + **out = **in + } + if in.MaxScaleDenominator != nil { + in, out := &in.MaxScaleDenominator, &out.MaxScaleDenominator + *out = new(string) + **out = **in + } + if in.Styles != nil { + in, out := &in.Styles, &out.Styles + *out = make([]Style, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = new(Data) + (*in).DeepCopyInto(*out) + } + if in.Layers != nil { + in, out := &in.Layers, &out.Layers + *out = make([]Layer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Layer. +func (in *Layer) DeepCopy() *Layer { + if in == nil { + return nil + } + out := new(Layer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Legend) DeepCopyInto(out *Legend) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Legend. +func (in *Legend) DeepCopy() *Legend { + if in == nil { + return nil + } + out := new(Legend) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mapfile) DeepCopyInto(out *Mapfile) { + *out = *in + in.ConfigMapKeyRef.DeepCopyInto(&out.ConfigMapKeyRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mapfile. +func (in *Mapfile) DeepCopy() *Mapfile { + if in == nil { + return nil + } + out := new(Mapfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Metadata) DeepCopyInto(out *Metadata) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metadata. +func (in *Metadata) DeepCopy() *Metadata { + if in == nil { + return nil + } + out := new(Metadata) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetadataURL) DeepCopyInto(out *MetadataURL) { + *out = *in + if in.CSW != nil { + in, out := &in.CSW, &out.CSW + *out = new(Metadata) + **out = **in + } + if in.Custom != nil { + in, out := &in.Custom, &out.Custom + *out = new(Custom) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetadataURL. +func (in *MetadataURL) DeepCopy() *MetadataURL { + if in == nil { + return nil + } + out := new(MetadataURL) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Options) DeepCopyInto(out *Options) { + *out = *in + out.BaseOptions = in.BaseOptions + out.WMSOptions = in.WMSOptions +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Options. +func (in *Options) DeepCopy() *Options { + if in == nil { + return nil + } + out := new(Options) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Postgis) DeepCopyInto(out *Postgis) { + *out = *in + if in.Columns != nil { + in, out := &in.Columns, &out.Columns + *out = make([]Column, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Postgis. +func (in *Postgis) DeepCopy() *Postgis { + if in == nil { + return nil + } + out := new(Postgis) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Style) DeepCopyInto(out *Style) { + *out = *in + if in.Title != nil { + in, out := &in.Title, &out.Title + *out = new(string) + **out = **in + } + if in.Abstract != nil { + in, out := &in.Abstract, &out.Abstract + *out = new(string) + **out = **in + } + if in.Visualization != nil { + in, out := &in.Visualization, &out.Visualization + *out = new(string) + **out = **in + } + if in.Legend != nil { + in, out := &in.Legend, &out.Legend + *out = new(Legend) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Style. +func (in *Style) DeepCopy() *Style { + if in == nil { + return nil + } + out := new(Style) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StylingAssets) DeepCopyInto(out *StylingAssets) { + *out = *in + if in.BlobKeys != nil { + in, out := &in.BlobKeys, &out.BlobKeys + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ConfigMapRefs != nil { + in, out := &in.ConfigMapRefs, &out.ConfigMapRefs + *out = make([]ConfigMapRef, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StylingAssets. +func (in *StylingAssets) DeepCopy() *StylingAssets { + if in == nil { + return nil + } + out := new(StylingAssets) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TIF) DeepCopyInto(out *TIF) { + *out = *in + if in.Offsite != nil { + in, out := &in.Offsite, &out.Offsite + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TIF. +func (in *TIF) DeepCopy() *TIF { + if in == nil { + return nil + } + out := new(TIF) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WFS) DeepCopyInto(out *WFS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFS. @@ -51,6 +709,22 @@ func (in *WFS) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WFSInspire) DeepCopyInto(out *WFSInspire) { + *out = *in + in.Inspire.DeepCopyInto(&out.Inspire) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSInspire. +func (in *WFSInspire) DeepCopy() *WFSInspire { + if in == nil { + return nil + } + out := new(WFSInspire) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WFSList) DeepCopyInto(out *WFSList) { *out = *in @@ -84,31 +758,88 @@ func (in *WFSList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { +func (in *WFSService) DeepCopyInto(out *WFSService) { *out = *in + in.BaseService.DeepCopyInto(&out.BaseService) + if in.Inspire != nil { + in, out := &in.Inspire, &out.Inspire + *out = new(WFSInspire) + (*in).DeepCopyInto(*out) + } + if in.OtherCrs != nil { + in, out := &in.OtherCrs, &out.OtherCrs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Bbox != nil { + in, out := &in.Bbox, &out.Bbox + *out = new(Bbox) + **out = **in + } + if in.CountDefault != nil { + in, out := &in.CountDefault, &out.CountDefault + *out = new(int) + **out = **in + } + if in.FeatureTypes != nil { + in, out := &in.FeatureTypes, &out.FeatureTypes + *out = make([]FeatureType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. -func (in *WFSSpec) DeepCopy() *WFSSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSService. +func (in *WFSService) DeepCopy() *WFSService { if in == nil { return nil } - out := new(WFSSpec) + out := new(WFSService) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WFSStatus) DeepCopyInto(out *WFSStatus) { +func (in *WFSSpec) DeepCopyInto(out *WFSSpec) { *out = *in + if in.Lifecycle != nil { + in, out := &in.Lifecycle, &out.Lifecycle + *out = new(model.Lifecycle) + (*in).DeepCopyInto(*out) + } + in.PodSpecPatch.DeepCopyInto(&out.PodSpecPatch) + if in.HorizontalPodAutoscalerPatch != nil { + in, out := &in.HorizontalPodAutoscalerPatch, &out.HorizontalPodAutoscalerPatch + *out = new(HorizontalPodAutoscalerPatch) + (*in).DeepCopyInto(*out) + } + if in.Options != nil { + in, out := &in.Options, &out.Options + *out = new(BaseOptions) + **out = **in + } + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(HealthCheckWFS) + **out = **in + } + if in.IngressRouteURLs != nil { + in, out := &in.IngressRouteURLs, &out.IngressRouteURLs + *out = make(model.IngressRouteURLs, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Service.DeepCopyInto(&out.Service) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSStatus. -func (in *WFSStatus) DeepCopy() *WFSStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WFSSpec. +func (in *WFSSpec) DeepCopy() *WFSSpec { if in == nil { return nil } - out := new(WFSStatus) + out := new(WFSSpec) in.DeepCopyInto(out) return out } @@ -118,8 +849,8 @@ func (in *WMS) DeepCopyInto(out *WMS) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMS. @@ -140,6 +871,22 @@ func (in *WMS) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WMSBoundingBox) DeepCopyInto(out *WMSBoundingBox) { + *out = *in + out.BBox = in.BBox +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSBoundingBox. +func (in *WMSBoundingBox) DeepCopy() *WMSBoundingBox { + if in == nil { + return nil + } + out := new(WMSBoundingBox) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *WMSList) DeepCopyInto(out *WMSList) { *out = *in @@ -173,31 +920,107 @@ func (in *WMSList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSSpec) DeepCopyInto(out *WMSSpec) { +func (in *WMSOptions) DeepCopyInto(out *WMSOptions) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSSpec. -func (in *WMSSpec) DeepCopy() *WMSSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSOptions. +func (in *WMSOptions) DeepCopy() *WMSOptions { if in == nil { return nil } - out := new(WMSSpec) + out := new(WMSOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WMSService) DeepCopyInto(out *WMSService) { + *out = *in + in.BaseService.DeepCopyInto(&out.BaseService) + if in.Inspire != nil { + in, out := &in.Inspire, &out.Inspire + *out = new(Inspire) + (*in).DeepCopyInto(*out) + } + if in.MaxSize != nil { + in, out := &in.MaxSize, &out.MaxSize + *out = new(int32) + **out = **in + } + if in.Resolution != nil { + in, out := &in.Resolution, &out.Resolution + *out = new(int32) + **out = **in + } + if in.DefResolution != nil { + in, out := &in.DefResolution, &out.DefResolution + *out = new(int32) + **out = **in + } + if in.StylingAssets != nil { + in, out := &in.StylingAssets, &out.StylingAssets + *out = new(StylingAssets) + (*in).DeepCopyInto(*out) + } + if in.Mapfile != nil { + in, out := &in.Mapfile, &out.Mapfile + *out = new(Mapfile) + (*in).DeepCopyInto(*out) + } + in.Layer.DeepCopyInto(&out.Layer) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSService. +func (in *WMSService) DeepCopy() *WMSService { + if in == nil { + return nil + } + out := new(WMSService) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *WMSStatus) DeepCopyInto(out *WMSStatus) { +func (in *WMSSpec) DeepCopyInto(out *WMSSpec) { *out = *in + if in.Lifecycle != nil { + in, out := &in.Lifecycle, &out.Lifecycle + *out = new(model.Lifecycle) + (*in).DeepCopyInto(*out) + } + in.PodSpecPatch.DeepCopyInto(&out.PodSpecPatch) + if in.HorizontalPodAutoscalerPatch != nil { + in, out := &in.HorizontalPodAutoscalerPatch, &out.HorizontalPodAutoscalerPatch + *out = new(HorizontalPodAutoscalerPatch) + (*in).DeepCopyInto(*out) + } + if in.Options != nil { + in, out := &in.Options, &out.Options + *out = new(Options) + **out = **in + } + if in.HealthCheck != nil { + in, out := &in.HealthCheck, &out.HealthCheck + *out = new(HealthCheckWMS) + (*in).DeepCopyInto(*out) + } + if in.IngressRouteURLs != nil { + in, out := &in.IngressRouteURLs, &out.IngressRouteURLs + *out = make(model.IngressRouteURLs, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Service.DeepCopyInto(&out.Service) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSStatus. -func (in *WMSStatus) DeepCopy() *WMSStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WMSSpec. +func (in *WMSSpec) DeepCopy() *WMSSpec { if in == nil { return nil } - out := new(WMSStatus) + out := new(WMSSpec) in.DeepCopyInto(out) return out } diff --git a/build-push-deploy-locally.sh b/build-push-deploy-locally.sh new file mode 100755 index 0000000..cc44f30 --- /dev/null +++ b/build-push-deploy-locally.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +TAG=$1 + +echo "Running: make generate" +make generate + +echo "" +echo "Running: build -t local-registry:5000/mapserver-operator:$TAG --build-context repos=./.. ." +docker build -t "local-registry:5000/mapserver-operator:$TAG" --build-context repos=./.. . + +echo "" +echo "Running: push local-registry:5000/mapserver-operator:$TAG" +docker push "local-registry:5000/mapserver-operator:$TAG" + +if [[ $(kubectl get pod -l app=webhook -n cert-manager | grep "cert-manager") ]]; then + echo "Cert-manager already installed" +else + echo "" + echo "Installing cert-manager" + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml +fi + +echo "Waiting for cert-manager" +while [[ $(kubectl get pod -l app=webhook -n cert-manager -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" ]]; do + sleep 1 +done +echo "Cert-manager ready" + +echo "" +echo "Running: make install" +make install + +echo "" +echo "Running: deploy IMG=local-registry:5000/mapserver-operator:$TAG" +make deploy "IMG=local-registry:5000/mapserver-operator:$TAG" \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index df0355c..7d195f4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -18,9 +18,21 @@ package main import ( "crypto/tls" + "errors" "flag" "os" - "path/filepath" + + "github.com/pdok/mapserver-operator/internal/controller/types" + + "github.com/go-logr/zapr" + "github.com/pdok/smooth-operator/pkg/integrations/logging" + "github.com/peterbourgon/ff" + "go.uber.org/zap/zapcore" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/pdok/mapserver-operator/internal/controller/mapfilegenerator" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -30,20 +42,20 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/certwatcher" "sigs.k8s.io/controller-runtime/pkg/healthz" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" - pdoknlv2beta1 "github.com/pdok/mapserver-operator/api/v2beta1" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" "github.com/pdok/mapserver-operator/internal/controller" webhookpdoknlv3 "github.com/pdok/mapserver-operator/internal/webhook/v3" // +kubebuilder:scaffold:imports ) +const ( + EnvFalse = "false" +) + var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") @@ -51,23 +63,29 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - + utilruntime.Must(traefikiov1alpha1.AddToScheme(scheme)) + utilruntime.Must(smoothoperatorv1.AddToScheme(scheme)) utilruntime.Must(pdoknlv3.AddToScheme(scheme)) - utilruntime.Must(pdoknlv2beta1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } -// nolint:gocyclo +//nolint:funlen func main() { var metricsAddr string - var metricsCertPath, metricsCertName, metricsCertKey string - var webhookCertPath, webhookCertName, webhookCertKey string + var certDir string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) - flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ + var host string + var mapserverDebugLevel int + var multitoolImage, mapfileGeneratorImage, mapserverImage, capabilitiesGeneratorImage, featureinfoGeneratorImage, ogcWebserviceProxyImage, apacheExporterImage string + var slackWebhookURL string + var logLevel int + var setUptimeOperatorAnnotations bool + var storageClassName string + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, @@ -75,22 +93,60 @@ func main() { "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") - flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") - flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") - flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") - flag.StringVar(&metricsCertPath, "metrics-cert-path", "", - "The directory that contains the metrics server certificate.") - flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") - flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + flag.StringVar(&certDir, "cert-dir", "", "CertDir contains the webhook server key and certificate. Defaults to /k8s-webhook-server/serving-certs.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.StringVar(&host, "baseurl", "", "The host which is used in the mapserver service.") + flag.StringVar(&multitoolImage, "multitool-image", "", "The image to use in the blob download init-container.") + flag.StringVar(&mapfileGeneratorImage, "mapfile-generator-image", "", "The image to use in the mapfile generator init-container.") + flag.StringVar(&mapserverImage, "mapserver-image", "", "The image to use in the mapserver container.") + flag.StringVar(&capabilitiesGeneratorImage, "capabilities-generator-image", "", "The image to use in the capabilities generator init-container.") + flag.StringVar(&featureinfoGeneratorImage, "featureinfo-generator-image", "", "The image to use in the featureinfo generator init-container.") + flag.StringVar(&ogcWebserviceProxyImage, "ogc-webservice-proxy-image", "", "The image to use in the ogc webservice proxy container.") + flag.StringVar(&apacheExporterImage, "apache-exporter-image", "", "The image to use in the apache-exporter container.") + flag.IntVar(&mapserverDebugLevel, "mapserver-debug-level", 0, "Debug level for the mapserver container, between 0 (error only) and 5 (very very verbose).") + flag.StringVar(&slackWebhookURL, "slack-webhook-url", "", "The webhook url for sending slack messages. Disabled if left empty") + flag.IntVar(&logLevel, "log-level", 0, "The zapcore loglevel. 0 = info, 1 = warn, 2 = error") + flag.BoolVar(&setUptimeOperatorAnnotations, "set-uptime-operator-annotations", true, "When enabled IngressRoutes get annotations that are used by the pdok/uptime-operator.") + flag.StringVar(&storageClassName, "storage-class-name", "", "The name of the storage class to use when using an ephemeral volume.") + opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) - flag.Parse() - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + if err := ff.Parse(flag.CommandLine, os.Args[1:], ff.WithEnvVarNoPrefix()); err != nil { + setupLog.Error(err, "unable to parse flags") + os.Exit(1) + } + + //nolint:gosec + levelEnabler := zapcore.Level(logLevel) + zapLogger, _ := logging.SetupLogger("mapserver-operator", slackWebhookURL, levelEnabler) + logrLogger := zapr.NewLogger(zapLogger) + ctrl.SetLogger(logrLogger) + + reqFlags := make(map[string]string) + reqFlags["baseurl"] = host + reqFlags["multitool-image"] = multitoolImage + reqFlags["mapfile-generator-image"] = mapfileGeneratorImage + reqFlags["mapserver-image"] = mapserverImage + reqFlags["capabilities-generator-image"] = capabilitiesGeneratorImage + reqFlags["featureinfo-generator-image"] = featureinfoGeneratorImage + reqFlags["ogc-webservice-proxy-image"] = ogcWebserviceProxyImage + reqFlags["apache-exporter-image"] = apacheExporterImage + + for reqFlag, val := range reqFlags { + if val == "" { + setupLog.Error(errors.New(reqFlag+" is a required flag"), "A value for "+reqFlag+" must be specified.") + os.Exit(1) + } + } + + pdoknlv3.SetHost(host) + mapfilegenerator.SetDebugLevel(mapserverDebugLevel) + controller.SetUptimeOperatorAnnotations(setUptimeOperatorAnnotations) + controller.SetStorageClassName(storageClassName) // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will @@ -107,83 +163,18 @@ func main() { tlsOpts = append(tlsOpts, disableHTTP2) } - // Create watchers for metrics and webhooks certificates - var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher - - // Initial webhook TLS options - webhookTLSOpts := tlsOpts - - if len(webhookCertPath) > 0 { - setupLog.Info("Initializing webhook certificate watcher using provided certificates", - "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) - - var err error - webhookCertWatcher, err = certwatcher.New( - filepath.Join(webhookCertPath, webhookCertName), - filepath.Join(webhookCertPath, webhookCertKey), - ) - if err != nil { - setupLog.Error(err, "Failed to initialize webhook certificate watcher") - os.Exit(1) - } - - webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { - config.GetCertificate = webhookCertWatcher.GetCertificate - }) - } - webhookServer := webhook.NewServer(webhook.Options{ - TLSOpts: webhookTLSOpts, + CertDir: certDir, + TLSOpts: tlsOpts, }) - // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. - // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/server - // - https://book.kubebuilder.io/reference/metrics.html - metricsServerOptions := metricsserver.Options{ - BindAddress: metricsAddr, - SecureServing: secureMetrics, - TLSOpts: tlsOpts, - } - - if secureMetrics { - // FilterProvider is used to protect the metrics endpoint with authn/authz. - // These configurations ensure that only authorized users and service accounts - // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/filters#WithAuthenticationAndAuthorization - metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization - } - - // If the certificate is not specified, controller-runtime will automatically - // generate self-signed certificates for the metrics server. While convenient for development and testing, - // this setup is not recommended for production. - // - // TODO(user): If you enable certManager, uncomment the following lines: - // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates - // managed by cert-manager for the metrics server. - // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. - if len(metricsCertPath) > 0 { - setupLog.Info("Initializing metrics certificate watcher using provided certificates", - "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) - - var err error - metricsCertWatcher, err = certwatcher.New( - filepath.Join(metricsCertPath, metricsCertName), - filepath.Join(metricsCertPath, metricsCertKey), - ) - if err != nil { - setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) - os.Exit(1) - } - - metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { - config.GetCertificate = metricsCertWatcher.GetCertificate - }) - } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ - Scheme: scheme, - Metrics: metricsServerOptions, + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + SecureServing: secureMetrics, + TLSOpts: tlsOpts, + }, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, @@ -208,6 +199,15 @@ func main() { if err = (&controller.WMSReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + Images: types.Images{ + MultitoolImage: multitoolImage, + MapfileGeneratorImage: mapfileGeneratorImage, + MapserverImage: mapserverImage, + CapabilitiesGeneratorImage: capabilitiesGeneratorImage, + FeatureinfoGeneratorImage: featureinfoGeneratorImage, + OgcWebserviceProxyImage: ogcWebserviceProxyImage, + ApacheExporterImage: apacheExporterImage, + }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "WMS") os.Exit(1) @@ -215,41 +215,33 @@ func main() { if err = (&controller.WFSReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), + Images: types.Images{ + MultitoolImage: multitoolImage, + MapfileGeneratorImage: mapfileGeneratorImage, + MapserverImage: mapserverImage, + CapabilitiesGeneratorImage: capabilitiesGeneratorImage, + ApacheExporterImage: apacheExporterImage, + }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "WFS") os.Exit(1) } - // nolint:goconst - if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = webhookpdoknlv3.SetupWMSWebhookWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create webhook", "webhook", "WMS") - os.Exit(1) - } - } - // nolint:goconst - if os.Getenv("ENABLE_WEBHOOKS") != "false" { + + if os.Getenv("ENABLE_WEBHOOKS") != EnvFalse { if err = webhookpdoknlv3.SetupWFSWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "WFS") os.Exit(1) } } - // +kubebuilder:scaffold:builder - if metricsCertWatcher != nil { - setupLog.Info("Adding metrics certificate watcher to manager") - if err := mgr.Add(metricsCertWatcher); err != nil { - setupLog.Error(err, "unable to add metrics certificate watcher to manager") + if os.Getenv("ENABLE_WEBHOOKS") != EnvFalse { + if err = webhookpdoknlv3.SetupWMSWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "WMS") os.Exit(1) } } - if webhookCertWatcher != nil { - setupLog.Info("Adding webhook certificate watcher to manager") - if err := mgr.Add(webhookCertWatcher); err != nil { - setupLog.Error(err, "unable to add webhook certificate watcher to manager") - os.Exit(1) - } - } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") diff --git a/config/crd/bases/embed.go b/config/crd/bases/embed.go new file mode 100644 index 0000000..45044dd --- /dev/null +++ b/config/crd/bases/embed.go @@ -0,0 +1,51 @@ +package bases + +import ( + _ "embed" + + "github.com/pdok/smooth-operator/pkg/validation" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/yaml" +) + +//go:embed pdok.nl_wfs.yaml +var wfsCRD []byte + +//go:embed pdok.nl_wms.yaml +var wmsCRD []byte + +func init() { + wms, err := GetWmsCRD() + if err != nil { + panic(err) + } + + err = validation.AddValidator(wms) + if err != nil { + panic(err) + } + + wfs, err := GetWfsCRD() + if err != nil { + panic(err) + } + + err = validation.AddValidator(wfs) + if err != nil { + panic(err) + } +} + +func GetWmsCRD() (v1.CustomResourceDefinition, error) { + crd := v1.CustomResourceDefinition{} + err := yaml.Unmarshal(wmsCRD, &crd) + + return crd, err +} + +func GetWfsCRD() (v1.CustomResourceDefinition, error) { + crd := v1.CustomResourceDefinition{} + err := yaml.Unmarshal(wfsCRD, &crd) + + return crd, err +} diff --git a/config/crd/bases/pdok.nl_wfs.yaml b/config/crd/bases/pdok.nl_wfs.yaml new file mode 100644 index 0000000..2d9378f --- /dev/null +++ b/config/crd/bases/pdok.nl_wfs.yaml @@ -0,0 +1,1185 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1 + creationTimestamp: null + name: wfs.pdok.nl +spec: + group: pdok.nl + names: + categories: + - pdok + kind: WFS + listKind: WFSList + plural: wfs + singular: wfs + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.podSummary[0].ready + name: ReadyPods + type: integer + - jsonPath: .status.podSummary[0].total + name: DesiredPods + type: integer + - jsonPath: .status.conditions[?(@.type == "Reconciled")].reason + name: ReconcileStatus + type: string + name: v3 + schema: + openAPIV3Schema: + description: WFS is the Schema for the wfs API. + 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: WFSSpec vertegenwoordigt de hoofdstruct voor de YAML-configuratie + properties: + healthCheck: + description: Custom healthcheck options + properties: + mimetype: + pattern: (image/png|text/xml|text/html) + type: string + querystring: + type: string + x-kubernetes-validations: + - message: a valid healthcheck contains 'Service=WFS' + rule: self.lowerAscii().contains('service=wfs') + - message: a valid healthcheck contains 'Request=' + rule: self.lowerAscii().contains('request=') + required: + - mimetype + - querystring + type: object + horizontalPodAutoscalerPatch: + description: |- + HorizontalPodAutoscalerPatch - copy of autoscalingv2.HorizontalPodAutoscalerSpec without ScaleTargetRef + This way we don't have to specify the scaleTargetRef field in the CRD. + properties: + behavior: + description: |- + HorizontalPodAutoscalerBehavior configures the scaling behavior of the target + in both Up and Down directions (scaleUp and scaleDown fields respectively). + properties: + scaleDown: + description: |- + scaleDown is scaling policy for scaling Down. + If not set, the default value is to allow to scale down to minReplicas pods, with a + 300 second stabilization window (i.e., the highest recommendation for + the last 300sec is used). + properties: + policies: + description: |- + policies is a list of potential scaling polices which can be used during scaling. + If not set, use the default values: + - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. + - For scale down: allow all pods to be removed in a 15s window. + items: + description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. + properties: + periodSeconds: + description: |- + periodSeconds specifies the window of time for which the policy should hold true. + PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). + format: int32 + type: integer + type: + description: type is used to specify the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling up or scaling down. + StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). + If not set, use the default values: + - For scale up: 0 (i.e. no stabilization is done). + - For scale down: 300 (i.e. the stabilization window is 300 seconds long). + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to the desired number of + replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not + set, the default cluster-wide tolerance is applied (by default 10%). + + For example, if autoscaling is configured with a memory consumption target of 100Mi, + and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be + triggered when the actual consumption falls below 95Mi or exceeds 101Mi. + + This is an alpha field and requires enabling the HPAConfigurableTolerance + feature gate. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: |- + scaleUp is scaling policy for scaling Up. + If not set, the default value is the higher of: + * increase no more than 4 pods per 60 seconds + * double the number of pods per 60 seconds + No stabilization is used. + properties: + policies: + description: |- + policies is a list of potential scaling polices which can be used during scaling. + If not set, use the default values: + - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. + - For scale down: allow all pods to be removed in a 15s window. + items: + description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. + properties: + periodSeconds: + description: |- + periodSeconds specifies the window of time for which the policy should hold true. + PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). + format: int32 + type: integer + type: + description: type is used to specify the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling up or scaling down. + StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). + If not set, use the default values: + - For scale up: 0 (i.e. no stabilization is done). + - For scale down: 300 (i.e. the stabilization window is 300 seconds long). + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to the desired number of + replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not + set, the default cluster-wide tolerance is applied (by default 10%). + + For example, if autoscaling is configured with a memory consumption target of 100Mi, + and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be + triggered when the actual consumption falls below 95Mi or exceeds 101Mi. + + This is an alpha field and requires enabling the HPAConfigurableTolerance + feature gate. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + format: int32 + type: integer + metrics: + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at once). + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing a single container in + each pod of the current scale target (e.g. CPU or memory). Such metrics are + built in to Kubernetes, and have special scaling options on top of those + available to normal per-pod metrics using the "pods" source. + properties: + container: + description: container is the name of the container in the pods of the scaling target + type: string + name: + description: name is the name of the resource in question. + type: string + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. It allows autoscaling based on information + coming from components running outside of cluster + (for example length of queue in cloud messaging service, or + QPS from loadbalancer running outside of cluster). + properties: + metric: + description: metric identifies the target metric by name and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the descriptions of a object,such as kind,name apiVersion + properties: + apiVersion: + description: apiVersion is the API version of the referent + type: string + kind: + description: 'kind is the kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'name is the name of the referent; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric by name and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example, transactions-processed-per-second). The values will be + averaged together before being compared to the target value. + properties: + metric: + description: metric identifies the target metric by name and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing each pod in the + current scale target (e.g. CPU or memory). Such metrics are built in to + Kubernetes, and have special scaling options on top of those available + to normal per-pod metrics using the "pods" source. + properties: + name: + description: name is the name of the resource in question. + type: string + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: |- + type is the type of metric source. It should be one of "ContainerResource", "External", + "Object", "Pods" or "Resource", each mapping to a matching field in the object. + type: string + required: + - type + type: object + type: array + minReplicas: + format: int32 + type: integer + type: object + ingressRouteUrls: + description: |- + Optional list of URLs where the service can be reached + By default only the spec.service.url is used + items: + properties: + url: + pattern: ^https?://.+/.+ + type: string + required: + - url + type: object + maxItems: 30 + minItems: 1 + type: array + lifecycle: + description: Optional lifecycle settings + properties: + ttlInDays: + format: int32 + type: integer + type: object + options: + description: Options configures optional behaviors of the operator, like ingress, casing, and data prefetching. + properties: + automaticCasing: + default: true + description: AutomaticCasing enables automatic conversion from snake_case to camelCase. + type: boolean + includeIngress: + default: true + description: IncludeIngress dictates whether to deploy an Ingress or ensure none exists. + type: boolean + prefetchData: + default: true + description: |- + Whether to prefetch data from blob storage, and store it on the local filesystem. + If `false`, the data will be served directly out of blob storage + type: boolean + type: object + podSpecPatch: + description: Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. + type: object + x-kubernetes-preserve-unknown-fields: true + service: + description: service configuration + properties: + abstract: + description: Service abstract + minLength: 1 + type: string + accessConstraints: + default: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + description: AccessConstraints URL + pattern: ^https?://.+/.+ + type: string + bbox: + description: Service bounding box + properties: + defaultCRS: + description: EXTENT/wfs_extent in mapfile + properties: + maxx: + description: Rechtsonder X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + maxy: + description: Rechtsonder Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + minx: + description: Linksboven X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + miny: + description: Linksboven Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + required: + - maxx + - maxy + - minx + - miny + type: object + required: + - defaultCRS + type: object + countDefault: + description: CountDefault -> wfs_maxfeatures in mapfile + minimum: 1 + type: integer + defaultCrs: + description: Default CRS (DataEPSG) + pattern: ^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$ + type: string + featureTypes: + description: FeatureTypes configurations + items: + description: FeatureType defines a WFS feature + properties: + abstract: + description: Abstract of the feature + minLength: 1 + type: string + bbox: + description: Optional feature bbox + properties: + defaultCRS: + description: DefaultCRS defines the EXTENT/wfs_extent for the featureType for use in the mapfile + properties: + maxx: + description: Rechtsonder X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + maxy: + description: Rechtsonder Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + minx: + description: Linksboven X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + miny: + description: Linksboven Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + required: + - maxx + - maxy + - minx + - miny + type: object + wgs84: + description: WGS84, if provided, gives the same bounding box reprojected into EPSG:4326 for use in the capabilities. + properties: + maxx: + description: Rechtsonder X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + maxy: + description: Rechtsonder Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + minx: + description: Linksboven X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + miny: + description: Linksboven Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + required: + - maxx + - maxy + - minx + - miny + type: object + type: object + data: + description: FeatureType data connection + properties: + gpkg: + description: Gpkg configures a GeoPackage file source + properties: + blobKey: + description: Blobkey identifies the location/bucket of the .gpkg file + pattern: ^.+\/.+\/.+\.gpkg$ + type: string + columns: + description: Columns to visualize for this table + items: + description: Column maps a source column name to an optional alias for output. + properties: + alias: + description: Alias for the column in the service output. + minLength: 1 + type: string + name: + description: Name of the column in the data source. + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + geometryType: + description: GeometryType of the table, must match an OGC type + pattern: ^(Multi)?(Point|LineString|Polygon)$ + type: string + tableName: + description: TableName is the table within the geopackage + minLength: 1 + type: string + required: + - blobKey + - columns + - geometryType + - tableName + type: object + postgis: + description: Postgis configures a Postgis table source + properties: + columns: + description: Columns to expose from table + items: + description: Column maps a source column name to an optional alias for output. + properties: + alias: + description: Alias for the column in the service output. + minLength: 1 + type: string + name: + description: Name of the column in the data source. + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + geometryType: + description: GeometryType of the table + pattern: ^(Multi)?(Point|LineString|Polygon)$ + type: string + tableName: + description: TableName in postGIS + minLength: 1 + type: string + required: + - columns + - geometryType + - tableName + type: object + type: object + x-kubernetes-validations: + - message: At least one of the datasource should be provided (postgis, gpkg) + rule: has(self.gpkg) || has(self.postgis) + datasetMetadataUrl: + description: Metadata URL + properties: + csw: + description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. + properties: + metadataIdentifier: + description: MetadataIdentifier is the record's UUID + pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ + type: string + required: + - metadataIdentifier + type: object + custom: + description: Custom allows arbitrary href + properties: + href: + description: Href of the custom metadata url + pattern: ^https?://.+/.+ + type: string + type: + description: MIME type of the custom link + minLength: 1 + type: string + required: + - href + - type + type: object + type: object + x-kubernetes-validations: + - message: metadataUrl should have exactly 1 of csw or custom + rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) + keywords: + description: Keywords of the feature + items: + minLength: 1 + type: string + minItems: 1 + type: array + name: + description: Name of the feature + pattern: ^\S+$ + type: string + title: + description: Title of the feature + minLength: 1 + type: string + required: + - abstract + - data + - keywords + - name + - title + type: object + minItems: 1 + type: array + fees: + description: Optional Fees + minLength: 1 + type: string + inspire: + description: Inspire holds INSPIRE-specific metadata for the service. + properties: + language: + description: Language of the INSPIRE metadata record + pattern: bul|cze|dan|dut|eng|est|fin|fre|ger|gre|hun|gle|ita|lav|lit|mlt|pol|por|rum|slo|slv|spa|swe + type: string + serviceMetadataUrl: + description: ServiceMetadataURL references the CSW or custom metadata record. + properties: + csw: + description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. + properties: + metadataIdentifier: + description: MetadataIdentifier is the record's UUID + pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ + type: string + required: + - metadataIdentifier + type: object + custom: + description: Custom allows arbitrary href + properties: + href: + description: Href of the custom metadata url + pattern: ^https?://.+/.+ + type: string + type: + description: MIME type of the custom link + minLength: 1 + type: string + required: + - href + - type + type: object + type: object + x-kubernetes-validations: + - message: metadataUrl should have exactly 1 of csw or custom + rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) + spatialDatasetIdentifier: + description: SpatialDatasetIdentifier is the ID uniquely identifying the dataset. + pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ + type: string + required: + - language + - serviceMetadataUrl + - spatialDatasetIdentifier + type: object + keywords: + description: Keywords for capabilities + items: + minLength: 1 + type: string + minItems: 1 + type: array + mapfile: + description: External Mapfile reference + properties: + configMapKeyRef: + description: Selects a key from a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or its key must be defined + type: boolean + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + required: + - configMapKeyRef + type: object + otherCrs: + description: Other supported CRS + items: + pattern: ^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$ + type: string + minItems: 1 + type: array + ownerInfoRef: + description: Reference to OwnerInfo CR + minLength: 1 + type: string + prefix: + description: Geonovum subdomein + minLength: 1 + type: string + title: + description: Service title + minLength: 1 + type: string + url: + description: URL of the service + pattern: ^https?://.+/.+ + type: string + required: + - abstract + - defaultCrs + - featureTypes + - keywords + - ownerInfoRef + - prefix + - title + - url + type: object + x-kubernetes-validations: + - fieldPath: .otherCrs + message: otherCrs can't contain the defaultCrs + rule: '!has(self.otherCrs) || (has(self.otherCrs) && !(self.defaultCrs in self.otherCrs))' + required: + - podSpecPatch + - service + type: object + x-kubernetes-validations: + - messageExpression: '''ingressRouteUrls should include service.url ''+self.service.url' + rule: '!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)' + status: + description: OperatorStatus defines the observed state of an Atom/WFS/WMS/OGCAPI/... + properties: + conditions: + description: |- + Each condition contains details for one aspect of the current state of this CR. + Known .status.conditions.type are: "Reconciled" + items: + description: Condition contains details for one aspect of the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + operationResults: + additionalProperties: + description: OperationResult is the action result of a CreateOrUpdate or CreateOrPatch call. + type: string + description: The result of creating or updating of each derived resource for this CR. + type: object + podSummary: + description: Summary of status of pods that belong to this CR + items: + properties: + available: + format: int32 + type: integer + generation: + format: int32 + type: integer + ready: + format: int32 + type: integer + total: + format: int32 + type: integer + unavailable: + format: int32 + type: integer + required: + - available + - generation + - ready + - total + - unavailable + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/pdok.nl_wms.yaml b/config/crd/bases/pdok.nl_wms.yaml new file mode 100644 index 0000000..35ed9aa --- /dev/null +++ b/config/crd/bases/pdok.nl_wms.yaml @@ -0,0 +1,1782 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.17.1 + creationTimestamp: null + name: wms.pdok.nl +spec: + group: pdok.nl + names: + categories: + - pdok + kind: WMS + listKind: WMSList + plural: wms + singular: wms + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.podSummary[0].ready + name: ReadyPods + type: integer + - jsonPath: .status.podSummary[0].total + name: DesiredPods + type: integer + - jsonPath: .status.conditions[?(@.type == "Reconciled")].reason + name: ReconcileStatus + type: string + name: v3 + schema: + openAPIV3Schema: + description: WMS is the Schema for the wms API. + 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: WMSSpec defines the desired state of WMS. + properties: + healthCheck: + description: Custom healthcheck options + properties: + boundingbox: + description: BBox defines a bounding box with coordinates + properties: + maxx: + description: Rechtsonder X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + maxy: + description: Rechtsonder Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + minx: + description: Linksboven X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + miny: + description: Linksboven Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + required: + - maxx + - maxy + - minx + - miny + type: object + mimetype: + pattern: (image/png|text/xml|text/html) + type: string + querystring: + type: string + x-kubernetes-validations: + - message: a valid healthcheck contains 'SERVICE=WMS' + rule: self.lowerAscii().contains('service=wms') + - message: a valid healthcheck contains 'REQUEST=' + rule: self.lowerAscii().contains('request=') + type: object + x-kubernetes-validations: + - message: mimetype is required when a querystring is used + rule: '!has(self.querystring) || has(self.mimetype)' + - message: healthcheck should have exactly 1 of querystring + mimetype or boundingbox + rule: (has(self.boundingbox) || has(self.querystring)) && !(has(self.querystring) && has(self.boundingbox)) + - message: healthcheck should have exactly 1 of querystring + mimetype or boundingbox + rule: (has(self.boundingbox) || has(self.mimetype)) && !(has(self.mimetype) && has(self.boundingbox)) + horizontalPodAutoscalerPatch: + description: Optional specification for the HorizontalAutoscaler + properties: + behavior: + description: |- + HorizontalPodAutoscalerBehavior configures the scaling behavior of the target + in both Up and Down directions (scaleUp and scaleDown fields respectively). + properties: + scaleDown: + description: |- + scaleDown is scaling policy for scaling Down. + If not set, the default value is to allow to scale down to minReplicas pods, with a + 300 second stabilization window (i.e., the highest recommendation for + the last 300sec is used). + properties: + policies: + description: |- + policies is a list of potential scaling polices which can be used during scaling. + If not set, use the default values: + - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. + - For scale down: allow all pods to be removed in a 15s window. + items: + description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. + properties: + periodSeconds: + description: |- + periodSeconds specifies the window of time for which the policy should hold true. + PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). + format: int32 + type: integer + type: + description: type is used to specify the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling up or scaling down. + StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). + If not set, use the default values: + - For scale up: 0 (i.e. no stabilization is done). + - For scale down: 300 (i.e. the stabilization window is 300 seconds long). + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to the desired number of + replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not + set, the default cluster-wide tolerance is applied (by default 10%). + + For example, if autoscaling is configured with a memory consumption target of 100Mi, + and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be + triggered when the actual consumption falls below 95Mi or exceeds 101Mi. + + This is an alpha field and requires enabling the HPAConfigurableTolerance + feature gate. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + scaleUp: + description: |- + scaleUp is scaling policy for scaling Up. + If not set, the default value is the higher of: + * increase no more than 4 pods per 60 seconds + * double the number of pods per 60 seconds + No stabilization is used. + properties: + policies: + description: |- + policies is a list of potential scaling polices which can be used during scaling. + If not set, use the default values: + - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window. + - For scale down: allow all pods to be removed in a 15s window. + items: + description: HPAScalingPolicy is a single policy which must hold true for a specified past interval. + properties: + periodSeconds: + description: |- + periodSeconds specifies the window of time for which the policy should hold true. + PeriodSeconds must be greater than zero and less than or equal to 1800 (30 min). + format: int32 + type: integer + type: + description: type is used to specify the scaling policy. + type: string + value: + description: |- + value contains the amount of change which is permitted by the policy. + It must be greater than zero + format: int32 + type: integer + required: + - periodSeconds + - type + - value + type: object + type: array + x-kubernetes-list-type: atomic + selectPolicy: + description: |- + selectPolicy is used to specify which policy should be used. + If not set, the default value Max is used. + type: string + stabilizationWindowSeconds: + description: |- + stabilizationWindowSeconds is the number of seconds for which past recommendations should be + considered while scaling up or scaling down. + StabilizationWindowSeconds must be greater than or equal to zero and less than or equal to 3600 (one hour). + If not set, use the default values: + - For scale up: 0 (i.e. no stabilization is done). + - For scale down: 300 (i.e. the stabilization window is 300 seconds long). + format: int32 + type: integer + tolerance: + anyOf: + - type: integer + - type: string + description: |- + tolerance is the tolerance on the ratio between the current and desired + metric value under which no updates are made to the desired number of + replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not + set, the default cluster-wide tolerance is applied (by default 10%). + + For example, if autoscaling is configured with a memory consumption target of 100Mi, + and scale-down and scale-up tolerances of 5% and 1% respectively, scaling will be + triggered when the actual consumption falls below 95Mi or exceeds 101Mi. + + This is an alpha field and requires enabling the HPAConfigurableTolerance + feature gate. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + maxReplicas: + format: int32 + type: integer + metrics: + items: + description: |- + MetricSpec specifies how to scale based on a single metric + (only `type` and one other matching field should be set at once). + properties: + containerResource: + description: |- + containerResource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing a single container in + each pod of the current scale target (e.g. CPU or memory). Such metrics are + built in to Kubernetes, and have special scaling options on top of those + available to normal per-pod metrics using the "pods" source. + properties: + container: + description: container is the name of the container in the pods of the scaling target + type: string + name: + description: name is the name of the resource in question. + type: string + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - container + - name + - target + type: object + external: + description: |- + external refers to a global metric that is not associated + with any Kubernetes object. It allows autoscaling based on information + coming from components running outside of cluster + (for example length of queue in cloud messaging service, or + QPS from loadbalancer running outside of cluster). + properties: + metric: + description: metric identifies the target metric by name and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + object: + description: |- + object refers to a metric describing a single kubernetes object + (for example, hits-per-second on an Ingress object). + properties: + describedObject: + description: describedObject specifies the descriptions of a object,such as kind,name apiVersion + properties: + apiVersion: + description: apiVersion is the API version of the referent + type: string + kind: + description: 'kind is the kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'name is the name of the referent; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + required: + - kind + - name + type: object + metric: + description: metric identifies the target metric by name and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - describedObject + - metric + - target + type: object + pods: + description: |- + pods refers to a metric describing each pod in the current scale target + (for example, transactions-processed-per-second). The values will be + averaged together before being compared to the target value. + properties: + metric: + description: metric identifies the target metric by name and selector + properties: + name: + description: name is the name of the given metric + type: string + selector: + description: |- + selector is the string-encoded form of a standard kubernetes label selector for the given metric + When set, it is passed as an additional parameter to the metrics server for more specific metrics scoping. + When unset, just the metricName will be used to gather metrics. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + required: + - name + type: object + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - metric + - target + type: object + resource: + description: |- + resource refers to a resource metric (such as those specified in + requests and limits) known to Kubernetes describing each pod in the + current scale target (e.g. CPU or memory). Such metrics are built in to + Kubernetes, and have special scaling options on top of those available + to normal per-pod metrics using the "pods" source. + properties: + name: + description: name is the name of the resource in question. + type: string + target: + description: target specifies the target value for the given metric + properties: + averageUtilization: + description: |- + averageUtilization is the target value of the average of the + resource metric across all relevant pods, represented as a percentage of + the requested value of the resource for the pods. + Currently only valid for Resource metric source type + format: int32 + type: integer + averageValue: + anyOf: + - type: integer + - type: string + description: |- + averageValue is the target value of the average of the + metric across all relevant pods (as a quantity) + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: type represents whether the metric type is Utilization, Value, or AverageValue + type: string + value: + anyOf: + - type: integer + - type: string + description: value is the target value of the metric (as a quantity). + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + required: + - type + type: object + required: + - name + - target + type: object + type: + description: |- + type is the type of metric source. It should be one of "ContainerResource", "External", + "Object", "Pods" or "Resource", each mapping to a matching field in the object. + type: string + required: + - type + type: object + type: array + minReplicas: + format: int32 + type: integer + type: object + ingressRouteUrls: + description: |- + Optional list of URLs where the service can be reached + By default only the spec.service.url is used + items: + properties: + url: + pattern: ^https?://.+/.+ + type: string + required: + - url + type: object + maxItems: 30 + minItems: 1 + type: array + lifecycle: + description: Optional lifecycle settings + properties: + ttlInDays: + format: int32 + type: integer + type: object + options: + description: Optional options for the configuration of the service. + properties: + automaticCasing: + default: true + description: AutomaticCasing enables automatic conversion from snake_case to camelCase. + type: boolean + disableWebserviceProxy: + default: false + description: DisableWebserviceProxy disables the built-in proxy for external web services. + type: boolean + includeIngress: + default: true + description: IncludeIngress dictates whether to deploy an Ingress or ensure none exists. + type: boolean + prefetchData: + default: true + description: |- + Whether to prefetch data from blob storage, and store it on the local filesystem. + If `false`, the data will be served directly out of blob storage + type: boolean + rewriteGroupToDataLayers: + default: false + description: RewriteGroupToDataLayers merges group layers into individual data layers. + type: boolean + validateChildStyleNameEqual: + default: false + description: ValidateChildStyleNameEqual ensures child style names match the parent style. + type: boolean + validateRequests: + default: true + description: ValidateRequests enables request validation against the service schema. + type: boolean + type: object + podSpecPatch: + description: Strategic merge patch for the pod in the deployment. E.g. to patch the resources or add extra env vars. + type: object + x-kubernetes-preserve-unknown-fields: true + service: + description: Service specification + properties: + abstract: + description: Service abstract + minLength: 1 + type: string + accessConstraints: + default: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + description: AccessConstraints URL + pattern: ^https?://.+/.+ + type: string + dataEPSG: + description: CRS of the data + pattern: (EPSG|CRS):\d+ + type: string + defResolution: + description: 'Mapfile setting: Sets the DEFRESOLUTION field in the mapfile, not used when service.mapfile is configured' + format: int32 + type: integer + fees: + description: Optional Fees + minLength: 1 + type: string + inspire: + description: Config for Inspire services + properties: + language: + description: Language of the INSPIRE metadata record + pattern: bul|cze|dan|dut|eng|est|fin|fre|ger|gre|hun|gle|ita|lav|lit|mlt|pol|por|rum|slo|slv|spa|swe + type: string + serviceMetadataUrl: + description: ServiceMetadataURL references the CSW or custom metadata record. + properties: + csw: + description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. + properties: + metadataIdentifier: + description: MetadataIdentifier is the record's UUID + pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ + type: string + required: + - metadataIdentifier + type: object + custom: + description: Custom allows arbitrary href + properties: + href: + description: Href of the custom metadata url + pattern: ^https?://.+/.+ + type: string + type: + description: MIME type of the custom link + minLength: 1 + type: string + required: + - href + - type + type: object + type: object + x-kubernetes-validations: + - message: metadataUrl should have exactly 1 of csw or custom + rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) + required: + - language + - serviceMetadataUrl + type: object + keywords: + description: Keywords for capabilities + items: + minLength: 1 + type: string + minItems: 1 + type: array + layer: + description: Toplayer + properties: + abstract: + description: Abstract of the layer + minLength: 1 + type: string + authority: + properties: + name: + type: string + spatialDatasetIdentifier: + type: string + url: + type: string + required: + - name + - spatialDatasetIdentifier + - url + type: object + boundingBoxes: + description: BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. + items: + properties: + bbox: + description: BBox defines a bounding box with coordinates + properties: + maxx: + description: Rechtsonder X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + maxy: + description: Rechtsonder Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + minx: + description: Linksboven X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + miny: + description: Linksboven Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + required: + - maxx + - maxy + - minx + - miny + type: object + crs: + pattern: ^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$ + type: string + required: + - bbox + - crs + type: object + minItems: 1 + type: array + datasetMetadataUrl: + description: Links to metadata + properties: + csw: + description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. + properties: + metadataIdentifier: + description: MetadataIdentifier is the record's UUID + pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ + type: string + required: + - metadataIdentifier + type: object + custom: + description: Custom allows arbitrary href + properties: + href: + description: Href of the custom metadata url + pattern: ^https?://.+/.+ + type: string + type: + description: MIME type of the custom link + minLength: 1 + type: string + required: + - href + - type + type: object + type: object + x-kubernetes-validations: + - message: metadataUrl should have exactly 1 of csw or custom + rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) + keywords: + description: Keywords of the layer, required if the layer is visible + items: + minLength: 1 + type: string + minItems: 1 + type: array + layers: + description: '[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]' + items: + description: Toplayer + properties: + abstract: + description: Abstract of the layer + minLength: 1 + type: string + authority: + properties: + name: + type: string + spatialDatasetIdentifier: + type: string + url: + type: string + required: + - name + - spatialDatasetIdentifier + - url + type: object + boundingBoxes: + description: BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. + items: + properties: + bbox: + description: BBox defines a bounding box with coordinates + properties: + maxx: + description: Rechtsonder X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + maxy: + description: Rechtsonder Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + minx: + description: Linksboven X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + miny: + description: Linksboven Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + required: + - maxx + - maxy + - minx + - miny + type: object + crs: + pattern: ^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$ + type: string + required: + - bbox + - crs + type: object + minItems: 1 + type: array + data: + description: Data (gpkg/postgis/tif) used by the layer + properties: + gpkg: + description: Gpkg configures a GeoPackage file source + properties: + blobKey: + description: Blobkey identifies the location/bucket of the .gpkg file + pattern: ^.+\/.+\/.+\.gpkg$ + type: string + columns: + description: Columns to visualize for this table + items: + description: Column maps a source column name to an optional alias for output. + properties: + alias: + description: Alias for the column in the service output. + minLength: 1 + type: string + name: + description: Name of the column in the data source. + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + geometryType: + description: GeometryType of the table, must match an OGC type + pattern: ^(Multi)?(Point|LineString|Polygon)$ + type: string + tableName: + description: TableName is the table within the geopackage + minLength: 1 + type: string + required: + - blobKey + - columns + - geometryType + - tableName + type: object + postgis: + description: Postgis configures a Postgis table source + properties: + columns: + description: Columns to expose from table + items: + description: Column maps a source column name to an optional alias for output. + properties: + alias: + description: Alias for the column in the service output. + minLength: 1 + type: string + name: + description: Name of the column in the data source. + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + geometryType: + description: GeometryType of the table + pattern: ^(Multi)?(Point|LineString|Polygon)$ + type: string + tableName: + description: TableName in postGIS + minLength: 1 + type: string + required: + - columns + - geometryType + - tableName + type: object + tif: + description: TIF configures a GeoTIF raster source + properties: + blobKey: + description: BlobKey to the TIFF file + pattern: ^.+\/.+\/.+\.(tif?f|vrt)$ + type: string + getFeatureInfoIncludesClass: + default: false + description: '"When a band represents nominal or ordinal data the class name (from styling) can be included in the getFeatureInfo"' + type: boolean + offsite: + description: Sets the color index to treat as transparent for raster layers, optional, hex or rgb + pattern: (#[0-9A-F]{6}([0-9A-F]{2})?)|([0-9]{1,3}\s[0-9]{1,3}\s[0-9]{1,3}) + type: string + oversampleRatio: + default: "2.5" + description: |- + Controls the smoothing of the image on a certain point. Bigger value gives a smoother/better picture but + results in slower web responses, optional + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + resample: + default: NEAREST + description: This option can be used to control the resampling kernel used sampling raster images, optional + pattern: (NEAREST|AVERAGE|BILINEAR) + type: string + required: + - blobKey + type: object + type: object + x-kubernetes-validations: + - message: Atleast one of the datasource should be provided (postgis, gpkg, tif) + rule: has(self.gpkg) || has(self.tif) || has(self.postgis) + datasetMetadataUrl: + description: Links to metadata + properties: + csw: + description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. + properties: + metadataIdentifier: + description: MetadataIdentifier is the record's UUID + pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ + type: string + required: + - metadataIdentifier + type: object + custom: + description: Custom allows arbitrary href + properties: + href: + description: Href of the custom metadata url + pattern: ^https?://.+/.+ + type: string + type: + description: MIME type of the custom link + minLength: 1 + type: string + required: + - href + - type + type: object + type: object + x-kubernetes-validations: + - message: metadataUrl should have exactly 1 of csw or custom + rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) + keywords: + description: Keywords of the layer, required if the layer is visible + items: + minLength: 1 + type: string + minItems: 1 + type: array + labelNoClip: + description: Mapfile setting, sets "LABEL_NO_CLIP=ON" + type: boolean + layers: + description: '[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]' + items: + description: Toplayer + properties: + abstract: + description: Abstract of the layer + minLength: 1 + type: string + authority: + properties: + name: + type: string + spatialDatasetIdentifier: + type: string + url: + type: string + required: + - name + - spatialDatasetIdentifier + - url + type: object + boundingBoxes: + description: BoundingBoxes of the layer. If omitted the boundingboxes of the parent layer of the service is used. + items: + properties: + bbox: + description: BBox defines a bounding box with coordinates + properties: + maxx: + description: Rechtsonder X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + maxy: + description: Rechtsonder Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + minx: + description: Linksboven X coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + miny: + description: Linksboven Y coรถrdinaat + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + required: + - maxx + - maxy + - minx + - miny + type: object + crs: + pattern: ^(EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)|CRS:84)$ + type: string + required: + - bbox + - crs + type: object + minItems: 1 + type: array + data: + description: Data (gpkg/postgis/tif) used by the layer + properties: + gpkg: + description: Gpkg configures a GeoPackage file source + properties: + blobKey: + description: Blobkey identifies the location/bucket of the .gpkg file + pattern: ^.+\/.+\/.+\.gpkg$ + type: string + columns: + description: Columns to visualize for this table + items: + description: Column maps a source column name to an optional alias for output. + properties: + alias: + description: Alias for the column in the service output. + minLength: 1 + type: string + name: + description: Name of the column in the data source. + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + geometryType: + description: GeometryType of the table, must match an OGC type + pattern: ^(Multi)?(Point|LineString|Polygon)$ + type: string + tableName: + description: TableName is the table within the geopackage + minLength: 1 + type: string + required: + - blobKey + - columns + - geometryType + - tableName + type: object + postgis: + description: Postgis configures a Postgis table source + properties: + columns: + description: Columns to expose from table + items: + description: Column maps a source column name to an optional alias for output. + properties: + alias: + description: Alias for the column in the service output. + minLength: 1 + type: string + name: + description: Name of the column in the data source. + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + geometryType: + description: GeometryType of the table + pattern: ^(Multi)?(Point|LineString|Polygon)$ + type: string + tableName: + description: TableName in postGIS + minLength: 1 + type: string + required: + - columns + - geometryType + - tableName + type: object + tif: + description: TIF configures a GeoTIF raster source + properties: + blobKey: + description: BlobKey to the TIFF file + pattern: ^.+\/.+\/.+\.(tif?f|vrt)$ + type: string + getFeatureInfoIncludesClass: + default: false + description: '"When a band represents nominal or ordinal data the class name (from styling) can be included in the getFeatureInfo"' + type: boolean + offsite: + description: Sets the color index to treat as transparent for raster layers, optional, hex or rgb + pattern: (#[0-9A-F]{6}([0-9A-F]{2})?)|([0-9]{1,3}\s[0-9]{1,3}\s[0-9]{1,3}) + type: string + oversampleRatio: + default: "2.5" + description: |- + Controls the smoothing of the image on a certain point. Bigger value gives a smoother/better picture but + results in slower web responses, optional + pattern: ^-?[0-9]+([.][0-9]*)?$ + type: string + resample: + default: NEAREST + description: This option can be used to control the resampling kernel used sampling raster images, optional + pattern: (NEAREST|AVERAGE|BILINEAR) + type: string + required: + - blobKey + type: object + type: object + x-kubernetes-validations: + - message: Atleast one of the datasource should be provided (postgis, gpkg, tif) + rule: has(self.gpkg) || has(self.tif) || has(self.postgis) + datasetMetadataUrl: + description: Links to metadata + properties: + csw: + description: CSW describes a metadata record via a metadataIdentifier (UUID) as defined in the OwnerInfo. + properties: + metadataIdentifier: + description: MetadataIdentifier is the record's UUID + pattern: ^[0-9a-zA-Z]{8}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{4}\-[0-9a-zA-Z]{12}$ + type: string + required: + - metadataIdentifier + type: object + custom: + description: Custom allows arbitrary href + properties: + href: + description: Href of the custom metadata url + pattern: ^https?://.+/.+ + type: string + type: + description: MIME type of the custom link + minLength: 1 + type: string + required: + - href + - type + type: object + type: object + x-kubernetes-validations: + - message: metadataUrl should have exactly 1 of csw or custom + rule: (has(self.csw) || has(self.custom)) && !(has(self.csw) && has(self.custom)) + keywords: + description: Keywords of the layer, required if the layer is visible + items: + minLength: 1 + type: string + minItems: 1 + type: array + labelNoClip: + description: Mapfile setting, sets "LABEL_NO_CLIP=ON" + type: boolean + maxscaledenominator: + description: The maximum scale at which this layer functions + pattern: ^[1-9][0-9]*(.[0-9]+)?$ + type: string + minscaledenominator: + description: The minimum scale at which this layer functions + pattern: ^[0-9]+(.[0-9]+)?$ + type: string + name: + description: Name of the layer, required for layers on the 2nd or 3rd level + minLength: 1 + type: string + styles: + description: List of styles used by the layer + items: + properties: + abstract: + minLength: 1 + type: string + legend: + properties: + blobKey: + description: Location of the legend on the blobstore + minLength: 1 + type: string + format: + default: image/png + description: Format of the legend, defaults to image/png + type: string + height: + description: The height of the legend in px, defaults to 20 + format: int32 + type: integer + width: + description: The width of the legend in px, defaults to 78 + format: int32 + type: integer + required: + - blobKey + type: object + name: + minLength: 1 + type: string + title: + minLength: 1 + type: string + visualization: + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + title: + description: Title of the layer + minLength: 1 + type: string + visible: + default: true + description: Whether or not the layer is visible. At least one of the layers must be visible. + type: boolean + required: + - name + - styles + type: object + x-kubernetes-validations: + - message: A layer with data attribute should have styling + rule: '!has(self.data) || has(self.styles)' + - message: A layer should have a title when visible + rule: '!self.visible || has(self.title)' + - message: A layer should have an abstract when visible + rule: '!self.visible || has(self.abstract)' + - message: A layer should have keywords when visible + rule: '!self.visible || has(self.keywords)' + minItems: 1 + type: array + maxscaledenominator: + description: The maximum scale at which this layer functions + pattern: ^[1-9][0-9]*(.[0-9]+)?$ + type: string + minscaledenominator: + description: The minimum scale at which this layer functions + pattern: ^[0-9]+(.[0-9]+)?$ + type: string + name: + description: Name of the layer, required for layers on the 2nd or 3rd level + minLength: 1 + type: string + styles: + description: List of styles used by the layer + items: + properties: + abstract: + minLength: 1 + type: string + legend: + properties: + blobKey: + description: Location of the legend on the blobstore + minLength: 1 + type: string + format: + default: image/png + description: Format of the legend, defaults to image/png + type: string + height: + description: The height of the legend in px, defaults to 20 + format: int32 + type: integer + width: + description: The width of the legend in px, defaults to 78 + format: int32 + type: integer + required: + - blobKey + type: object + name: + minLength: 1 + type: string + title: + minLength: 1 + type: string + visualization: + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + title: + description: Title of the layer + minLength: 1 + type: string + visible: + default: true + description: Whether or not the layer is visible. At least one of the layers must be visible. + type: boolean + required: + - name + - styles + type: object + x-kubernetes-validations: + - message: A layer should have exactly one of sublayers or data + rule: (has(self.data) || has(self.layers)) && !(has(self.data) && has(self.layers)) + - message: A layer with data attribute should have styling + rule: '!has(self.data) || has(self.styles)' + - message: A layer should have a title when visible + rule: '!self.visible || has(self.title)' + - message: A layer should have an abstract when visible + rule: '!self.visible || has(self.abstract)' + - message: A layer should have keywords when visible + rule: '!self.visible || has(self.keywords)' + minItems: 1 + type: array + maxscaledenominator: + description: The maximum scale at which this layer functions + pattern: ^[1-9][0-9]*(.[0-9]+)?$ + type: string + minscaledenominator: + description: The minimum scale at which this layer functions + pattern: ^[0-9]+(.[0-9]+)?$ + type: string + name: + description: Name of the layer, required for layers on the 2nd or 3rd level + minLength: 1 + type: string + styles: + description: List of styles used by the layer + items: + properties: + abstract: + minLength: 1 + type: string + legend: + properties: + blobKey: + description: Location of the legend on the blobstore + minLength: 1 + type: string + format: + default: image/png + description: Format of the legend, defaults to image/png + type: string + height: + description: The height of the legend in px, defaults to 20 + format: int32 + type: integer + width: + description: The width of the legend in px, defaults to 78 + format: int32 + type: integer + required: + - blobKey + type: object + name: + minLength: 1 + type: string + title: + minLength: 1 + type: string + visualization: + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + title: + description: Title of the layer + minLength: 1 + type: string + visible: + default: true + description: Whether or not the layer is visible. At least one of the layers must be visible. + type: boolean + required: + - title + - abstract + - keywords + - boundingBoxes + - layers + type: object + x-kubernetes-validations: + - fieldPath: .visible + message: TopLayer must be visible + rule: self.visible + - fieldPath: .styles + message: If TopLayer has a name, it must have styles + rule: '!has(self.name) || has(self.styles)' + mapfile: + description: External Mapfile reference + properties: + configMapKeyRef: + description: Selects a key from a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the referent. + type: string + optional: + description: Specify whether the ConfigMap or its key must be defined + type: boolean + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + required: + - configMapKeyRef + type: object + maxSize: + description: 'Mapfile setting: Sets the maximum size (in pixels) for both dimensions of the image from a getMap request.' + format: int32 + minimum: 1 + type: integer + ownerInfoRef: + description: Reference to OwnerInfo CR + minLength: 1 + type: string + prefix: + description: Geonovum subdomein + minLength: 1 + type: string + resolution: + description: 'Mapfile setting: Sets the RESOLUTION field in the mapfile, not used when service.mapfile is configured' + format: int32 + type: integer + stylingAssets: + description: Optional. Required files for the styling of the service + properties: + blobKeys: + description: 'BlobKeys contains symbol image (.png/.svg) or font (.ttf) keys on blob storage, format: container/key/file.(png|ttf)' + items: + pattern: ^.+\/.+\/.+\.(png|ttf|svg)$ + type: string + minItems: 1 + type: array + configMapRefs: + items: + properties: + keys: + description: Keys contains styling assets that contain mapfile code (.style|.symbol), required if you use symbols in your styles + items: + pattern: ^\S*.\.(style|symbol) + type: string + minItems: 1 + type: array + name: + description: Name is the name of the ConfigMap + minLength: 1 + type: string + required: + - name + type: object + minItems: 1 + type: array + type: object + x-kubernetes-validations: + - message: At least one of blobKeys or configMapRefs is required + rule: has(self.blobKeys) || has(self.configMapRefs) + title: + description: Service title + minLength: 1 + type: string + url: + description: URL of the service + pattern: ^https?://.+/.+ + type: string + required: + - abstract + - dataEPSG + - keywords + - layer + - ownerInfoRef + - prefix + - title + - url + type: object + x-kubernetes-validations: + - message: service requires styling, either through service.mapfile, or stylingAssets.configMapRefs + rule: has(self.mapfile) || (has(self.stylingAssets) && has(self.stylingAssets.configMapRefs)) + - message: when using service.mapfile, don't include stylingAssets.configMapRefs + rule: '!has(self.mapfile) || (!has(self.stylingAssets) || !has(self.stylingAssets.configMapRefs))' + required: + - podSpecPatch + - service + type: object + x-kubernetes-validations: + - messageExpression: '''ingressRouteUrls should include service.url ''+self.service.url' + rule: '!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.url)' + status: + description: OperatorStatus defines the observed state of an Atom/WFS/WMS/OGCAPI/... + properties: + conditions: + description: |- + Each condition contains details for one aspect of the current state of this CR. + Known .status.conditions.type are: "Reconciled" + items: + description: Condition contains details for one aspect of the current state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + operationResults: + additionalProperties: + description: OperationResult is the action result of a CreateOrUpdate or CreateOrPatch call. + type: string + description: The result of creating or updating of each derived resource for this CR. + type: object + podSummary: + description: Summary of status of pods that belong to this CR + items: + properties: + available: + format: int32 + type: integer + generation: + format: int32 + type: integer + ready: + format: int32 + type: integer + total: + format: int32 + type: integer + unavailable: + format: int32 + type: integer + required: + - available + - generation + - ready + - total + - unavailable + type: object + type: array + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 92271d9..14028ae 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,11 +6,11 @@ resources: - bases/pdok.nl_wfs.yaml # +kubebuilder:scaffold:crdkustomizeresource -patches: +# patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -- path: patches/webhook_in_wms.yaml -- path: patches/webhook_in_wfs.yaml +# - path: patches/webhook_in_wfs.yaml +# - path: patches/webhook_in_wms.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [WEBHOOK] To enable webhook, uncomment the following section diff --git a/config/crd/update_openapi.go b/config/crd/update_openapi.go new file mode 100644 index 0000000..0adbf1d --- /dev/null +++ b/config/crd/update_openapi.go @@ -0,0 +1,180 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + goyaml "gopkg.in/yaml.v3" + v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + kyaml "sigs.k8s.io/yaml" +) + +// Usage: go run ./update_layersv3_openapi.go +func main() { + crdDir := os.Args[1] + + updateWMSV3(crdDir) + updateWFSV3(crdDir) +} + +func updateWMSV3(crdDir string) { + path := filepath.Join(crdDir, "pdok.nl_wms.yaml") + + if _, err := os.Stat(path); os.IsNotExist(err) { + panic(errors.Wrap(err, "WMS v3 manifest not found")) + } + + content, _ := os.ReadFile(path) + crd := &v1.CustomResourceDefinition{} + err := kyaml.Unmarshal(content, &crd) + if err != nil { + panic(err) + } + + versions := make([]v1.CustomResourceDefinitionVersion, 0) + for _, version := range crd.Spec.Versions { + if version.Name == "v3" { + updateMapfileV3(&version) + updateLayersV3(&version) + + versions = append(versions, version) + } else { + versions = append(versions, version) + } + } + + crd.Spec.Versions = versions + updatedContent, _ := kyaml.Marshal(crd) + + // Remove the 'status' field from the yaml + var rawData map[string]interface{} + _ = goyaml.Unmarshal(updatedContent, &rawData) + delete(rawData, "status") + + f, _ := os.OpenFile(path, os.O_TRUNC|os.O_WRONLY, 0644) + defer f.Close() + + enc := goyaml.NewEncoder(f) + defer enc.Close() + + enc.SetIndent(2) + _ = enc.Encode(rawData) +} + +func updateLayersV3(version *v1.CustomResourceDefinitionVersion) { + schema := version.Schema.OpenAPIV3Schema + spec := schema.Properties["spec"] + service := spec.Properties["service"] + layer := service.Properties["layer"] + + // Level 3 + layerSpecLevel3 := layer.DeepCopy() + layerSpecLevel3.Required = append(layerSpecLevel3.Required, "name") + layerSpecLevel3.Required = append(layerSpecLevel3.Required, "styles") + delete(layerSpecLevel3.Properties, "layers") + xvals := v1.ValidationRules{} + for _, xval := range layerSpecLevel3.XValidations { + if !strings.Contains(xval.Rule, "self.layers") { + xvals = append(xvals, xval) + } + } + layerSpecLevel3.XValidations = xvals + + // Level 2 + layerSpecLevel2 := layer.DeepCopy() + layerSpecLevel2.Required = append(layerSpecLevel2.Required, "name") + layerSpecLevel2.Required = append(layerSpecLevel2.Required, "styles") + bottomLayers := layerSpecLevel2.Properties["layers"] + bottomLayers.Description = "[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]" + bottomLayers.Items = &v1.JSONSchemaPropsOrArray{Schema: layerSpecLevel3} + layerSpecLevel2.Properties["layers"] = bottomLayers + + // Level 1 + layerSpecLevel1 := layer.DeepCopy() + layerSpecLevel1.Required = append(layerSpecLevel1.Required, "title", "abstract", "keywords", "boundingBoxes", "layers") + layerSpecLevel1.XValidations = []v1.ValidationRule{ + {Rule: "self.visible", Message: "TopLayer must be visible", FieldPath: ".visible"}, + {Rule: "!has(self.name) || has(self.styles)", Message: "If TopLayer has a name, it must have styles", FieldPath: ".styles"}, + } + delete(layerSpecLevel1.Properties, "data") + delete(layerSpecLevel1.Properties, "labelNoClip") + + midLayers := layerSpecLevel1.Properties["layers"] + midLayers.Description = "[OpenAPI spec injected by mapserver-operator/cmd/update_openapi.go]" + midLayers.Items = &v1.JSONSchemaPropsOrArray{Schema: layerSpecLevel2} + + layerSpecLevel1.Properties["layers"] = midLayers + + service.Properties["layer"] = *layerSpecLevel1 + spec.Properties["service"] = service + schema.Properties["spec"] = spec + version.Schema = &v1.CustomResourceValidation{ + OpenAPIV3Schema: schema, + } +} + +func updateWFSV3(crdDir string) { + path := filepath.Join(crdDir, "pdok.nl_wfs.yaml") + + if _, err := os.Stat(path); os.IsNotExist(err) { + panic(errors.Wrap(err, "WFS v3 manifest not found")) + } + + content, _ := os.ReadFile(path) + crd := &v1.CustomResourceDefinition{} + err := kyaml.Unmarshal(content, &crd) + if err != nil { + panic(err) + } + + versions := make([]v1.CustomResourceDefinitionVersion, 0) + for _, version := range crd.Spec.Versions { + if version.Name == "v3" { + updateMapfileV3(&version) + + versions = append(versions, version) + } else { + versions = append(versions, version) + } + } + + crd.Spec.Versions = versions + updatedContent, _ := kyaml.Marshal(crd) + + // Remove the 'status' field from the yaml + var rawData map[string]interface{} + _ = goyaml.Unmarshal(updatedContent, &rawData) + delete(rawData, "status") + + f, _ := os.OpenFile(path, os.O_TRUNC|os.O_WRONLY, 0644) + defer f.Close() + + enc := goyaml.NewEncoder(f) + defer enc.Close() + + enc.SetIndent(2) + _ = enc.Encode(rawData) +} + +func updateMapfileV3(version *v1.CustomResourceDefinitionVersion) { + schema := version.Schema.OpenAPIV3Schema + spec := schema.Properties["spec"] + service := spec.Properties["service"] + mapfile := service.Properties["mapfile"] + configMapKeyRef := mapfile.Properties["configMapKeyRef"] + configMapKeyRef.Required = append(configMapKeyRef.Required, "name") + name := configMapKeyRef.Properties["name"] + name.Default = nil + name.Description = "Name of the referent." + + configMapKeyRef.Properties["name"] = name + mapfile.Properties["configMapKeyRef"] = configMapKeyRef + service.Properties["mapfile"] = mapfile + spec.Properties["service"] = service + schema.Properties["spec"] = spec + version.Schema = &v1.CustomResourceValidation{ + OpenAPIV3Schema: schema, + } +} diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index be72a73..1306759 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -22,7 +22,7 @@ resources: # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. @@ -37,9 +37,9 @@ resources: patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics -- path: manager_metrics_patch.yaml - target: - kind: Deployment +#- path: manager_metrics_patch.yaml +# target: +# kind: Deployment # Uncomment the patches line if you enable Metrics and CertManager # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. @@ -56,215 +56,193 @@ patches: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: -# - source: # Uncomment the following block to enable certificates for metrics -# kind: Service -# version: v1 -# name: controller-manager-metrics-service -# fieldPath: metadata.name -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: metrics-certs -# fieldPaths: -# - spec.dnsNames.0 -# - spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor -# kind: ServiceMonitor -# group: monitoring.coreos.com -# version: v1 -# name: controller-manager-metrics-monitor -# fieldPaths: -# - spec.endpoints.0.tlsConfig.serverName -# options: -# delimiter: '.' -# index: 0 -# create: true -# -# - source: -# kind: Service -# version: v1 -# name: controller-manager-metrics-service -# fieldPath: metadata.namespace -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: metrics-certs -# fieldPaths: -# - spec.dnsNames.0 -# - spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true -# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor -# kind: ServiceMonitor -# group: monitoring.coreos.com -# version: v1 -# name: controller-manager-metrics-monitor -# fieldPaths: -# - spec.endpoints.0.tlsConfig.serverName -# options: -# delimiter: '.' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have any webhook -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # Name of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # Namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# - select: -# kind: CustomResourceDefinition -# name: wms.pdok.nl -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - select: -# kind: CustomResourceDefinition -# name: wfs.pdok.nl -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true +replacements: + - source: # Uncomment the following block to enable certificates for metrics + kind: Service + version: v1 + name: controller-manager-metrics-service + fieldPath: metadata.name + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: metrics-certs + fieldPaths: + - spec.dnsNames.0 + - spec.dnsNames.1 + options: + delimiter: '.' + index: 0 + create: true + + - source: + kind: Service + version: v1 + name: controller-manager-metrics-service + fieldPath: metadata.namespace + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: metrics-certs + fieldPaths: + - spec.dnsNames.0 + - spec.dnsNames.1 + options: + delimiter: '.' + index: 1 + create: true + + - source: # Uncomment the following block if you have any webhook + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.name # Name of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 0 + create: true + - source: + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.namespace # Namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 1 + create: true + + - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + + - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: + - select: + kind: MutatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + + - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - select: + kind: CustomResourceDefinition + name: wfs.pdok.nl + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - select: + kind: CustomResourceDefinition + name: wms.pdok.nl + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true # +kubebuilder:scaffold:crdkustomizecainjectionns -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# - select: -# kind: CustomResourceDefinition -# name: wms.pdok.nl -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# - select: -# kind: CustomResourceDefinition -# name: wfs.pdok.nl -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - select: + kind: CustomResourceDefinition + name: wfs.pdok.nl + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + - select: + kind: CustomResourceDefinition + name: wms.pdok.nl + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true # +kubebuilder:scaffold:crdkustomizecainjectionname diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b8..ebe5985 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: local-registry:5000/mapserver-operator + newTag: v3.0.3 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 2e94912..82e4d3f 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -63,6 +63,8 @@ spec: args: - --leader-elect - --health-probe-bind-address=:8081 + - --baseurl=http://localhost:32788 + - --storage-class-name=test-storage image: controller:latest name: manager ports: [] diff --git a/config/prometheus/monitor_tls_patch.yaml b/config/prometheus/monitor_tls_patch.yaml index 5bf84ce..e824dd0 100644 --- a/config/prometheus/monitor_tls_patch.yaml +++ b/config/prometheus/monitor_tls_patch.yaml @@ -1,19 +1,22 @@ # Patch for Prometheus ServiceMonitor to enable secure TLS configuration # using certificates managed by cert-manager -- op: replace - path: /spec/endpoints/0/tlsConfig - value: - # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize - serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc - insecureSkipVerify: false - ca: - secret: - name: metrics-server-cert - key: ca.crt - cert: - secret: - name: metrics-server-cert - key: tls.crt - keySecret: - name: metrics-server-cert - key: tls.key +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: controller-manager-metrics-monitor + namespace: system +spec: + endpoints: + - tlsConfig: + insecureSkipVerify: false + ca: + secret: + name: metrics-server-cert + key: ca.crt + cert: + secret: + name: metrics-server-cert + key: tls.crt + keySecret: + name: metrics-server-cert + key: tls.key diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index c087f0f..64992be 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,6 +4,70 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - configmaps + - services + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get + - list + - watch +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - pdok.nl + resources: + - ownerinfo + verbs: + - get + - list + - watch +- apiGroups: + - pdok.nl + resources: + - ownerinfo/status + verbs: + - get - apiGroups: - pdok.nl resources: @@ -33,3 +97,38 @@ rules: - get - patch - update +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - list + - update + - watch +- apiGroups: + - policy + resources: + - poddisruptionbudgets/finalizers + verbs: + - update +- apiGroups: + - policy + resources: + - poddisruptionbudgets/status + verbs: + - get + - update +- apiGroups: + - traefik.io + resources: + - ingressroutes + - middlewares + verbs: + - create + - delete + - get + - list + - update + - watch diff --git a/config/samples/samples.go b/config/samples/samples.go new file mode 100644 index 0000000..48ae9f3 --- /dev/null +++ b/config/samples/samples.go @@ -0,0 +1,17 @@ +package samples + +import ( + _ "embed" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "sigs.k8s.io/yaml" +) + +//go:embed v3_wfs.yaml +var v3WFSContent string + +func V3WFS() (*pdoknlv3.WFS, error) { + var sample pdoknlv3.WFS + err := yaml.Unmarshal([]byte(v3WFSContent), &sample) + return &sample, err +} diff --git a/config/samples/v2beta1_wfs.yaml b/config/samples/v2beta1_wfs.yaml index db2924f..2e3191d 100644 --- a/config/samples/v2beta1_wfs.yaml +++ b/config/samples/v2beta1_wfs.yaml @@ -1,9 +1,63 @@ apiVersion: pdok.nl/v2beta1 kind: WFS metadata: + name: sample-v2 labels: app.kubernetes.io/name: mapserver-operator app.kubernetes.io/managed-by: kustomize - name: wfs-sample + dataset: dataset + dataset-owner: eigenaar + service-version: v1_0 + service-type: wfs + annotations: + lifecycle-phase: prod + service-bundle-id: e9f89184-d8c3-5600-8502-08e8e9bc9d2f spec: - # TODO(user): Add fields here + general: + datasetOwner: eigenaar + serviceVersion: v1_0 + dataset: dataset + kubernetes: + resources: + limits: + ephemeralStorage: 20Mi + options: + automaticCasing: true + includeIngress: true + service: + inspire: true + title: Dataset + abstract: "Dataset beschrijving..." + keywords: + - keyword1 + - keyword2 + accessConstraints: none + metadataIdentifier: 68a42961-ed55-436b-a412-cc7424fd2a6e + authority: + name: eigenaar + url: https://www.rijksoverheid.nl/ministeries/ministerie-van-economische-zaken-en-klimaat + dataEPSG: "EPSG:28992" + extent: "0 300000 280000 625000" + featureTypes: + - name: "feature1" + title: "feature1" + abstract: "Feature 1 beschrijving..." + keywords: + - keyword1 + - keyword2 + datasetMetadataIdentifier: "07d73b60-dfd6-4c54-9c82-9fac70c6c48e" + sourceMetadataIdentifier: "07d73b60-dfd6-4c54-9c82-9fac70c6c48e" # TODO + data: + gpkg: + blobKey: eigenaar/dataset/data.gpkg + table: "table1" + geometryType: "MultiPolygon" + columns: + - "naam" + - "gebiedsnum" + - "besluitnum" + - "besluitdat" + aliases: + gebiedsnum: gebiedsnummer + besluitdat: datum + diff --git a/config/samples/v3_wfs.yaml b/config/samples/v3_wfs.yaml index 4b3691b..06bb5cb 100644 --- a/config/samples/v3_wfs.yaml +++ b/config/samples/v3_wfs.yaml @@ -1,9 +1,42 @@ apiVersion: pdok.nl/v3 kind: WFS metadata: + name: sample labels: - app.kubernetes.io/name: mapserver-operator - app.kubernetes.io/managed-by: kustomize - name: wfs-sample + pdok.nl/owner-id: pdok + pdok.nl/dataset-id: sample spec: - # TODO(user): Add fields here + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 2G + service: + inspire: + language: dut + serviceMetadataUrl: + csw: + metadataIdentifier: 655549bd-8c05-4c69-950b-ad1e346dcac9 + spatialDatasetIdentifier: 90af202c-de3a-4fbf-901c-82ae703904e3 + title: "title" + abstract: "abstract" + defaultCrs: "EPSG:28992" + keywords: + - "keyword" + ownerInfoRef: "owner" + prefix: "prefix" + url: "http://host/path" + featureTypes: + - name: "name" + title: "title" + abstract: "abstract" + keywords: + - "word" + data: + gpkg: + blobKey: "container/prefix/file.gpkg" + columns: + - name: "column" + geometryType: "Point" + tableName: "table" diff --git a/config/samples/v3_wms.yaml b/config/samples/v3_wms.yaml index ed961c2..b4aae82 100644 --- a/config/samples/v3_wms.yaml +++ b/config/samples/v3_wms.yaml @@ -1,9 +1,70 @@ apiVersion: pdok.nl/v3 kind: WMS metadata: + name: sample labels: - app.kubernetes.io/name: mapserver-operator - app.kubernetes.io/managed-by: kustomize - name: wms-sample + pdok.nl/owner-id: pdok + pdok.nl/dataset-id: sample spec: - # TODO(user): Add fields here + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 1m + service: + prefix: "prefix" + url: "https://test.test/path" + title: "title" + abstract: "abstract" + keywords: + - "keyword" + ownerInfoRef: pdok + dataEPSG: "EPSG:28992" + stylingAssets: + configMapRefs: + - name: configmap + keys: + - file.symbol + layer: + title: "title" + abstract: "abstract" + keywords: + - "keyword" + visible: true + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + layers: + - name: "visible" + visible: true + title: "title" + abstract: "abstract" + keywords: + - keyword + data: + gpkg: + blobKey: "container/path/file.gpkg" + columns: + - name: "column" + geometryType: "Point" + tableName: "table" + styles: + - name: "name" + title: "title" + visualization: file.symbol + - name: "not visible" + visible: false + data: + postgis: + columns: + - name: "column" + geometryType: "Point" + tableName: "table" + styles: + - name: "name" + visualization: file.symbol diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 459a32b..490c033 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -24,3 +24,23 @@ webhooks: resources: - wfs sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-pdok-nl-v3-wms + failurePolicy: Fail + name: vwms-v3.kb.io + rules: + - apiGroups: + - pdok.nl + apiVersions: + - v3 + operations: + - CREATE + - UPDATE + resources: + - wms + sideEffects: None diff --git a/go.mod b/go.mod index 665a78d..192a905 100644 --- a/go.mod +++ b/go.mod @@ -1,100 +1,148 @@ module github.com/pdok/mapserver-operator -go 1.23.0 +go 1.24.0 + +toolchain go1.24.2 godebug default=go1.23 require ( - github.com/onsi/ginkgo/v2 v2.22.0 - github.com/onsi/gomega v1.36.1 - k8s.io/apimachinery v0.32.1 - k8s.io/client-go v0.32.1 - sigs.k8s.io/controller-runtime v0.20.2 + github.com/cbroglie/mustache v1.4.0 + github.com/onsi/ginkgo/v2 v2.23.4 + github.com/onsi/gomega v1.37.0 + github.com/pdok/featureinfo-generator v1.4.0 + github.com/pdok/ogc-capabilities-generator v1.0.1 + github.com/pdok/ogc-specifications v1.0.0 + github.com/pdok/smooth-operator v1.2.2 + github.com/peterbourgon/ff v1.7.1 + github.com/stretchr/testify v1.10.0 + github.com/traefik/traefik/v3 v3.4.1 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 + sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/yaml v1.4.0 +) + +replace github.com/abbot/go-http-auth => github.com/abbot/go-http-auth v0.4.0 // for github.com/traefik/traefik/v3 + +require ( + github.com/aws/smithy-go v1.22.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/go-acme/lego/v4 v4.23.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/google/go-github/v28 v28.1.1 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/http-wasm/http-wasm-host-go v0.7.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/miekg/dns v1.1.64 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/zerolog v1.33.0 // indirect + github.com/traefik/paerser v0.2.2 // indirect + github.com/unrolled/render v1.0.2 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.0 // indirect + go.etcd.io/etcd/client/v3 v3.6.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/otel/log v0.8.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.8.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/mod v0.24.0 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect ) require ( - cel.dev/expr v0.18.0 // indirect - github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + cel.dev/expr v0.24.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect; indirectC + github.com/fxamacker/cbor/v2 v2.8.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-logr/zapr v1.3.0 + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.22.0 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect + github.com/google/cel-go v0.25.0 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 + github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.7.0 // indirect - golang.org/x/tools v0.26.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.33.0 + gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.1 // indirect - k8s.io/apiextensions-apiserver v0.32.1 // indirect - k8s.io/apiserver v0.32.1 // indirect - k8s.io/component-base v0.32.1 // indirect + gopkg.in/yaml.v3 v3.0.1 + k8s.io/apiextensions-apiserver v0.33.1 + k8s.io/apiserver v0.33.1 // indirect + k8s.io/component-base v0.33.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.19.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect ) diff --git a/go.sum b/go.sum index b257e61..ac4bf9c 100644 --- a/go.sum +++ b/go.sum @@ -1,74 +1,110 @@ -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= -github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= -github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= +github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= +github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-acme/lego/v4 v4.23.1 h1:lZ5fGtGESA2L9FB8dNTvrQUq3/X4QOb8ExkKyY7LSV4= +github.com/go-acme/lego/v4 v4.23.1/go.mod h1:7UMVR7oQbIYw6V7mTgGwi4Er7B6Ww0c+c8feiBM0EgI= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= -github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo= +github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4= +github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/http-wasm/http-wasm-host-go v0.7.0 h1:+1KrRyOO6tWiDB24QrtSYyDmzFLBBs3jioKaUT0mq1c= +github.com/http-wasm/http-wasm-host-go v0.7.0/go.mod h1:adXKcLmL7yuavH/e0kBAp7b3TgAHTo/enCduyN5bXGM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -77,15 +113,26 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ= +github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -93,61 +140,113 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= -github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pdok/featureinfo-generator v1.4.0 h1:AqtLxdLj3nnBxjsnPCRKyddOyhq52h40OEjAbNJgIYE= +github.com/pdok/featureinfo-generator v1.4.0/go.mod h1:02Ryu7ZRkeha8SCfS6VYWCdKYZh0llskrfFgp6xRCjk= +github.com/pdok/ogc-capabilities-generator v1.0.1 h1:7cKgdcWO4y+M9a2hW8ONhmBxGERdva/WB4JBaB9+91s= +github.com/pdok/ogc-capabilities-generator v1.0.1/go.mod h1:Fnq2i1X3Aufjx/ptQ34c9knw1mDMUAQt0MTBscqX0/M= +github.com/pdok/ogc-specifications v1.0.0 h1:YqVqKwgOrPprTuRcNrFJeUuxSHOIeOSQStBcYJZUGcA= +github.com/pdok/ogc-specifications v1.0.0/go.mod h1:YDngwkwrWOfc5MYnEYseiv97K1Y9bZXlVzwi/8EaIl8= +github.com/pdok/smooth-operator v1.2.2 h1:g6wq77mbK335KMb73Hn/7LnRA8VVqtdr7hj+tjrdoiM= +github.com/pdok/smooth-operator v1.2.2/go.mod h1:tqr/CDCXZHNzQzQVlSAnCmsPlx9tWAObsj8hg9mSSEU= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/peterbourgon/ff v1.7.1 h1:xt1lxTG+Nr2+tFtysY7abFgPoH3Lug8CwYJMOmJRXhk= +github.com/peterbourgon/ff v1.7.1/go.mod h1:fYI5YA+3RDqQRExmFbHnBjEeWzh9TrS8rnRpEq7XIg0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= +github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/traefik/paerser v0.2.2 h1:cpzW/ZrQrBh3mdwD/jnp6aXASiUFKOVr6ldP+keJTcQ= +github.com/traefik/paerser v0.2.2/go.mod h1:7BBDd4FANoVgaTZG+yh26jI6CA2nds7D/4VTEdIsh24= +github.com/traefik/traefik/v3 v3.4.1 h1:QBO/C9ILViPVBhsmY8nEnoobTULxg6oW1jUTX8FFh8w= +github.com/traefik/traefik/v3 v3.4.1/go.mod h1:8FHoFbX5P+zMQ3UUjjfrDH87BDSbHllcUQyiI2wCP3o= +github.com/unrolled/render v1.0.2 h1:dGS3EmChQP3yOi1YeFNO/Dx+MbWZhdvhQJTXochM5bs= +github.com/unrolled/render v1.0.2/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.etcd.io/etcd/api/v3 v3.6.0 h1:vdbkcUBGLf1vfopoGE/uS3Nv0KPyIpUV/HM6w9yx2kM= +go.etcd.io/etcd/api/v3 v3.6.0/go.mod h1:Wt5yZqEmxgTNJGHob7mTVBJDZNXiHPtXTcPab37iFOw= +go.etcd.io/etcd/client/pkg/v3 v3.6.0 h1:nchnPqpuxvv3UuGGHaz0DQKYi5EIW5wOYsgUNRc365k= +go.etcd.io/etcd/client/pkg/v3 v3.6.0/go.mod h1:Jv5SFWMnGvIBn8o3OaBq/PnT0jjsX8iNokAUessNjoA= +go.etcd.io/etcd/client/v3 v3.6.0 h1:/yjKzD+HW5v/3DVj9tpwFxzNbu8hjcKID183ug9duWk= +go.etcd.io/etcd/client/v3 v3.6.0/go.mod h1:Jzk/Knqe06pkOZPHXsQ0+vNDvMQrgIqJ0W8DwPdMJMg= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/collector/pdata v1.10.0 h1:oLyPLGvPTQrcRT64ZVruwvmH/u3SHTfNo01pteS4WOE= +go.opentelemetry.io/collector/pdata v1.10.0/go.mod h1:IHxHsp+Jq/xfjORQMDJjSH6jvedOSTOyu3nbxqhWSYE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= +go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -157,56 +256,68 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= -gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= -google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= +gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -214,34 +325,42 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= -k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= -k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= -k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= -k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= -k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= -k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= -k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= -k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= -k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= -k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= +k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= +k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= +k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc= -sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= +k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 h1:qPrZsv1cwQiFeieFlRqT627fVZ+tyfou/+S5S0H5ua0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ= +sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o= +sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA= +sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 221dcbe..02c63b7 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,15 +1,23 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ \ No newline at end of file diff --git a/internal/controller/blobdownload/blob_download.go b/internal/controller/blobdownload/blob_download.go new file mode 100644 index 0000000..7c439f7 --- /dev/null +++ b/internal/controller/blobdownload/blob_download.go @@ -0,0 +1,225 @@ +package blobdownload + +import ( + _ "embed" + "fmt" + "regexp" + "strings" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + "github.com/pdok/mapserver-operator/internal/controller/types" + + "k8s.io/utils/strings/slices" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/mapperutils" + "github.com/pdok/mapserver-operator/internal/controller/utils" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +const ( + tifPath = "/srv/data/tif" + imagesPath = "/srv/data/images" + fontsPath = "/srv/data/config/fonts" + legendPath = "/var/www/legend" +) + +//go:embed gpkg_download.sh +var GpkgDownloadScript string + +func GetScript() string { + return GpkgDownloadScript +} + +func GetBlobDownloadInitContainer[O pdoknlv3.WMSWFS](obj O, images types.Images) (*corev1.Container, error) { + blobkeys := []string{} + for _, gpkg := range obj.GeoPackages() { + // Deduplicate blobkeys to prevent double downloads + if !slices.Contains(blobkeys, gpkg.BlobKey) { + blobkeys = append(blobkeys, gpkg.BlobKey) + } + } + + initContainer := corev1.Container{ + Name: constants.BlobDownloadName, + Image: images.MultitoolImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{ + { + Name: "GEOPACKAGE_TARGET_PATH", + Value: "/srv/data/gpkg", + }, + { + Name: "GEOPACKAGE_DOWNLOAD_LIST", + Value: strings.Join(blobkeys, ";"), + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("0.15"), + }, + }, + Command: []string{"/bin/sh", "-c"}, + VolumeMounts: []corev1.VolumeMount{ + utils.GetBaseVolumeMount(), + utils.GetDataVolumeMount(), + }, + } + + // Additional blob-download configuration + args, err := GetArgs(obj) + if err != nil { + return nil, err + } + initContainer.Args = []string{args} + + resourceCPU := resource.MustParse("0.2") + if use, _ := mapperutils.UseEphemeralVolume(obj); use { + resourceCPU = resource.MustParse("1") + } + initContainer.Resources.Limits = corev1.ResourceList{ + corev1.ResourceCPU: resourceCPU, + } + + if obj.Options().PrefetchData { + mount := corev1.VolumeMount{Name: constants.InitScriptsName, MountPath: "/srv/scripts", ReadOnly: true} + initContainer.VolumeMounts = append(initContainer.VolumeMounts, mount) + } + + return &initContainer, nil +} + +func GetArgs[W pdoknlv3.WMSWFS](webservice W) (args string, err error) { + var sb strings.Builder + + switch any(webservice).(type) { + case *pdoknlv3.WFS: + if WFS, ok := any(webservice).(*pdoknlv3.WFS); ok { + createConfig(&sb) + downloadGeopackage(&sb, WFS.Options().PrefetchData) + // In case of WFS no downloads are needed for TIFFs, styling assets and legends + } + case *pdoknlv3.WMS: + if WMS, ok := any(webservice).(*pdoknlv3.WMS); ok { + createConfig(&sb) + downloadGeopackage(&sb, WMS.Options().PrefetchData) + if err = downloadTiffs(&sb, WMS); err != nil { + return "", err + } + if err = downloadStylingAssets(&sb, WMS); err != nil { + return "", err + } + if err = downloadLegends(&sb, WMS); err != nil { + return "", err + } + } + default: + return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) + } + return sb.String(), nil +} + +func createConfig(sb *strings.Builder) { + writeLine(sb, "set -e;") + writeLine(sb, "mkdir -p /srv/data/config/;") + writeLine(sb, "rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true;") +} + +func downloadGeopackage(sb *strings.Builder, prefetchData bool) { + if prefetchData { + writeLine(sb, "bash /srv/scripts/gpkg_download.sh;") + } +} + +func downloadTiffs(sb *strings.Builder, wms *pdoknlv3.WMS) error { + if !wms.Options().PrefetchData { + return nil + } + + for _, blobKey := range wms.GetUniqueTiffBlobKeys() { + fileName, err := getFilenameFromBlobKey(blobKey) + if err != nil { + return err + } + writeLine(sb, "rclone copyto blobs:/%s %s/%s || exit 1;", blobKey, tifPath, fileName) + } + return nil +} + +func downloadStylingAssets(sb *strings.Builder, wms *pdoknlv3.WMS) error { + if wms.Spec.Service.StylingAssets == nil { // TODO Is StylingAssets required and should this return an error? + return nil + } + + generatedFontsList := false + re := regexp.MustCompile(`.*\.(ttf)$`) + for _, blobKey := range wms.Spec.Service.StylingAssets.BlobKeys { + fileName, err := getFilenameFromBlobKey(blobKey) + if err != nil { + return err + } + path := imagesPath + isTTF := re.MatchString(fileName) + if isTTF { + path = fontsPath + } + writeLine(sb, "rclone copyto blobs:/%s %s/%s || exit 1;", blobKey, path, fileName) + if isTTF { + fileRoot, err := getRootFromFilename(fileName) + if err != nil { + return err + } + writeLine(sb, "echo %s %s >> %s/fonts.list;", fileRoot, fileName, fontsPath) + generatedFontsList = true + } + } + + if generatedFontsList { + writeLine(sb, "echo 'generated fonts.list:';") + writeLine(sb, "cat %v/fonts.list;", fontsPath) + } + + return nil +} + +func downloadLegends(sb *strings.Builder, wms *pdoknlv3.WMS) error { + layers := wms.GetAllLayersWithLegend() + if len(layers) > 0 { + for _, layer := range layers { + writeLine(sb, "mkdir -p %s/%s;", legendPath, *layer.Name) + for _, style := range layer.Styles { + writeLine(sb, "rclone copyto blobs:/%s %s/%s/%s.png || exit 1;", style.Legend.BlobKey, legendPath, *layer.Name, style.Name) + fileName, err := getFilenameFromBlobKey(style.Legend.BlobKey) + if err != nil { + return err + } + writeLine(sb, "echo 'Copied legend %s to %s/%s/%s.png';", fileName, legendPath, *layer.Name, style.Name) + } + } + writeLine(sb, "chown -R 999:999 %s", legendPath) + } + + return nil +} + +func getFilenameFromBlobKey(blobKey string) (string, error) { + index := strings.LastIndex(blobKey, "/") + if index == -1 { + return "", fmt.Errorf("could not determine filename from blobkey %s", blobKey) + } + return blobKey[index+1:], nil +} + +func getRootFromFilename(fileName string) (string, error) { + index := strings.LastIndex(fileName, ".") + if index == -1 { + return "", fmt.Errorf("could not determine root from filename %s", fileName) + } + return fileName[:index], nil +} + +func writeLine(sb *strings.Builder, format string, a ...any) { + sb.WriteString(fmt.Sprintf(format, a...) + "\n") +} diff --git a/internal/controller/blobdownload/blob_download_test.go b/internal/controller/blobdownload/blob_download_test.go new file mode 100644 index 0000000..81b87b4 --- /dev/null +++ b/internal/controller/blobdownload/blob_download_test.go @@ -0,0 +1,332 @@ +package blobdownload + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +const ( + WFSArgsWithPrefetch = `set -e; +mkdir -p /srv/data/config/; +rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; +bash /srv/scripts/gpkg_download.sh; +` + WFSArgsWithoutPrefetch = `set -e; +mkdir -p /srv/data/config/; +rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; +` + + WMSArgsForGeoPackageLayers = `set -e; +mkdir -p /srv/data/config/; +rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; +bash /srv/scripts/gpkg_download.sh; +rclone copyto blobs:/resources-bucket/key/gpkg-symbol.png /srv/data/images/gpkg-symbol.png || exit 1; +rclone copyto blobs:/resources-bucket/key/symbol.svg /srv/data/images/symbol.svg || exit 1; +rclone copyto blobs:/resources-bucket/key/font-1.ttf /srv/data/config/fonts/font-1.ttf || exit 1; +echo font-1 font-1.ttf >> /srv/data/config/fonts/fonts.list; +rclone copyto blobs:/resources-bucket/key/font-2.ttf /srv/data/config/fonts/font-2.ttf || exit 1; +echo font-2 font-2.ttf >> /srv/data/config/fonts/fonts.list; +echo 'generated fonts.list:'; +cat /srv/data/config/fonts/fonts.list; +mkdir -p /var/www/legend/wms-gpkg-layer-1-name; +rclone copyto blobs:/resources-bucket/key/gpkg-layer-1-legend.png /var/www/legend/wms-gpkg-layer-1-name/wms-gpkg-style-1-name.png || exit 1; +echo 'Copied legend gpkg-layer-1-legend.png to /var/www/legend/wms-gpkg-layer-1-name/wms-gpkg-style-1-name.png'; +mkdir -p /var/www/legend/wms-gpkg-layer-2-name; +rclone copyto blobs:/resources-bucket/key/gpkg-layer-2-legend.png /var/www/legend/wms-gpkg-layer-2-name/wms-gpkg-style-2-name.png || exit 1; +echo 'Copied legend gpkg-layer-2-legend.png to /var/www/legend/wms-gpkg-layer-2-name/wms-gpkg-style-2-name.png'; +chown -R 999:999 /var/www/legend +` + + WMSArgsForTIFLayers = `set -e; +mkdir -p /srv/data/config/; +rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; +bash /srv/scripts/gpkg_download.sh; +rclone copyto blobs:/tifs-bucket/key/tif-layer-1-data.tif /srv/data/tif/tif-layer-1-data.tif || exit 1; +rclone copyto blobs:/tifs-bucket/key/tif-layer-2-data.tif /srv/data/tif/tif-layer-2-data.tif || exit 1; +rclone copyto blobs:/resources-bucket/key/tif-symbol.png /srv/data/images/tif-symbol.png || exit 1; +rclone copyto blobs:/resources-bucket/key/symbol.svg /srv/data/images/symbol.svg || exit 1; +rclone copyto blobs:/resources-bucket/key/font-1.ttf /srv/data/config/fonts/font-1.ttf || exit 1; +echo font-1 font-1.ttf >> /srv/data/config/fonts/fonts.list; +rclone copyto blobs:/resources-bucket/key/font-2.ttf /srv/data/config/fonts/font-2.ttf || exit 1; +echo font-2 font-2.ttf >> /srv/data/config/fonts/fonts.list; +echo 'generated fonts.list:'; +cat /srv/data/config/fonts/fonts.list; +mkdir -p /var/www/legend/wms-tif-layer-1-name; +rclone copyto blobs:/resources-bucket/key/tif-layer-1-legend.png /var/www/legend/wms-tif-layer-1-name/wms-tif-style-1-name.png || exit 1; +echo 'Copied legend tif-layer-1-legend.png to /var/www/legend/wms-tif-layer-1-name/wms-tif-style-1-name.png'; +mkdir -p /var/www/legend/wms-tif-layer-2-name; +rclone copyto blobs:/resources-bucket/key/tif-layer-2-legend.png /var/www/legend/wms-tif-layer-2-name/wms-tif-style-2-name.png || exit 1; +echo 'Copied legend tif-layer-2-legend.png to /var/www/legend/wms-tif-layer-2-name/wms-tif-style-2-name.png'; +chown -R 999:999 /var/www/legend +` +) + +func TestGetArgsForWFS(t *testing.T) { + type args struct { + WFS *pdoknlv3.WFS + } + tests := []struct { + name string + args args + wantArgs string + wantErr bool + }{ + { + name: "GetArgs for WFS with prefetchData", + args: args{ + WFS: &pdoknlv3.WFS{ + Spec: pdoknlv3.WFSSpec{ + Service: pdoknlv3.WFSService{BaseService: pdoknlv3.BaseService{ + Title: "wfs-prefetch-service-title", + }}, + Options: &pdoknlv3.BaseOptions{ + PrefetchData: true, + }, + }, + }, + }, + wantArgs: WFSArgsWithPrefetch, + wantErr: false, + }, + { + name: "GetArgs for WFS without prefetchData", + args: args{ + WFS: &pdoknlv3.WFS{ + Spec: pdoknlv3.WFSSpec{ + Service: pdoknlv3.WFSService{BaseService: pdoknlv3.BaseService{ + Title: "wfs-noprefetch-service-title", + }}, + Options: &pdoknlv3.BaseOptions{ + PrefetchData: false, + }, + }, + }, + }, + wantArgs: WFSArgsWithoutPrefetch, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args, err := GetArgs(tt.args.WFS) + if (err != nil) != tt.wantErr { + t.Errorf("GetArgs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if args == "" { + t.Errorf("The returned arguments are empty.") + } + if args != tt.wantArgs { + t.Errorf("GetArgs() = %v, want %v", args, tt.wantArgs) + return + } + }) + } +} + +func TestGetArgsForWMS(t *testing.T) { + type args struct { + WMS pdoknlv3.WMS + } + tests := []struct { + name string + args args + wantArgs string + wantErr bool + }{ + { + name: "GetArgs for WMS GeoPackage layer", + args: args{ + WMS: pdoknlv3.WMS{ + Spec: pdoknlv3.WMSSpec{ + Service: pdoknlv3.WMSService{BaseService: pdoknlv3.BaseService{ + Title: "wms-gpkg-service-title"}, + Layer: pdoknlv3.Layer{ + Name: smoothoperatorutils.Pointer("wms-gpkg-layer-name"), + Title: smoothoperatorutils.Pointer("wms-gpkg-layer-title"), + Styles: []pdoknlv3.Style{ + { + Legend: &pdoknlv3.Legend{ + BlobKey: "key/gpkg-layer-legend.png", + }, + }, + }, + Layers: []pdoknlv3.Layer{ + { + Name: smoothoperatorutils.Pointer("wms-gpkg-layer-1-name"), + Title: smoothoperatorutils.Pointer("wms-gpkg-layer-1-title"), + Styles: []pdoknlv3.Style{ + { + Name: "wms-gpkg-style-1-name", + Title: smoothoperatorutils.Pointer("wms-gpkg-style-1-title"), + Legend: &pdoknlv3.Legend{ + Width: 50, + Height: 50, + Format: "png", + BlobKey: "resources-bucket/key/gpkg-layer-1-legend.png", + }, + }, + }, + Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ + Gpkg: &pdoknlv3.Gpkg{ + BlobKey: "geopackages-bucket/key/gpkg-layer-1-data.gpkg", + }}, + }, + }, + { + Name: smoothoperatorutils.Pointer("wms-gpkg-layer-2-name"), + Title: smoothoperatorutils.Pointer("wms-gpkg-layer-2-title"), + Styles: []pdoknlv3.Style{ + { + Name: "wms-gpkg-style-2-name", + Title: smoothoperatorutils.Pointer("wms-gpkg-style-2-title"), + Legend: &pdoknlv3.Legend{ + BlobKey: "resources-bucket/key/gpkg-layer-2-legend.png", + }, + }, + }, + Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ + Gpkg: &pdoknlv3.Gpkg{ + BlobKey: "geopackages-bucket/key/gpkg-layer-2-data.gpkg", + }}, + }, + }, + }, + }, + StylingAssets: &pdoknlv3.StylingAssets{ + BlobKeys: []string{ + "resources-bucket/key/gpkg-symbol.png", + "resources-bucket/key/symbol.svg", + "resources-bucket/key/font-1.ttf", + "resources-bucket/key/font-2.ttf", + }, + }, + }, + Options: &pdoknlv3.Options{BaseOptions: pdoknlv3.BaseOptions{ + PrefetchData: true, + }}, + }, + }, + }, + wantArgs: WMSArgsForGeoPackageLayers, + wantErr: false, + }, + { + name: "GetArgs for WMS TIF layer", + args: args{ + WMS: pdoknlv3.WMS{ + Spec: pdoknlv3.WMSSpec{ + Service: pdoknlv3.WMSService{BaseService: pdoknlv3.BaseService{ + Title: "wms-tif-service-title"}, + Layer: pdoknlv3.Layer{ + Name: smoothoperatorutils.Pointer("wms-tif-layer-name"), + Title: smoothoperatorutils.Pointer("wms-tif-layer-title"), + Layers: []pdoknlv3.Layer{ + { + Name: smoothoperatorutils.Pointer("wms-tif-layer-1-name"), + Title: smoothoperatorutils.Pointer("wms-tif-layer-1-title"), + Styles: []pdoknlv3.Style{ + { + Name: "wms-tif-style-1-name", + Title: smoothoperatorutils.Pointer("wms-tif-style-1-title"), + Legend: &pdoknlv3.Legend{ + BlobKey: "resources-bucket/key/tif-layer-1-legend.png", + }, + }, + }, + Data: &pdoknlv3.Data{ + TIF: &pdoknlv3.TIF{ + BlobKey: "tifs-bucket/key/tif-layer-1-data.tif", + }, + }, + }, + { + Name: smoothoperatorutils.Pointer("wms-tif-layer-2-name"), + Title: smoothoperatorutils.Pointer("wms-tif-layer-2-title"), + Styles: []pdoknlv3.Style{ + { + Name: "wms-tif-style-2-name", + Title: smoothoperatorutils.Pointer("wms-tif-style-2-title"), + Legend: &pdoknlv3.Legend{ + BlobKey: "resources-bucket/key/tif-layer-2-legend.png", + }, + }, + }, + Data: &pdoknlv3.Data{ + TIF: &pdoknlv3.TIF{ + BlobKey: "tifs-bucket/key/tif-layer-2-data.tif", + }, + }, + }, + }, + }, + StylingAssets: &pdoknlv3.StylingAssets{ + BlobKeys: []string{ + "resources-bucket/key/tif-symbol.png", + "resources-bucket/key/symbol.svg", + "resources-bucket/key/font-1.ttf", + "resources-bucket/key/font-2.ttf", + }, + }, + }, + Options: &pdoknlv3.Options{BaseOptions: pdoknlv3.BaseOptions{ + PrefetchData: true, + }}, + }, + }, + }, + wantArgs: WMSArgsForTIFLayers, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args, err := GetArgs(&tt.args.WMS) + if (err != nil) != tt.wantErr { + t.Errorf("GetArgs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if args != tt.wantArgs { + diff := cmp.Diff(tt.wantArgs, args) + t.Errorf("GetArgs() -want, +got %s", diff) + return + } + }) + } +} + +func TestGetScript(t *testing.T) { + tests := []struct { + name string + wantHeader string + wantFunctions []string + wantErr bool + }{ + { + name: "Test for expected header and functions in the returned bash script", + wantHeader: "#!/usr/bin/env bash", + wantFunctions: []string{"download_gpkg", "download", "download_all", "rm_file_and_exit"}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + script := GetScript() + if !strings.HasPrefix(script, tt.wantHeader) { + t.Errorf("The returned script doesn't contain the expected header `%v`, got = %v", tt.wantHeader, script) + } + + for _, function := range tt.wantFunctions { + funcString := "function " + function + "()" + if !strings.Contains(script, funcString) { + t.Errorf("The returned script doesn't contain the expected function `%v`, got = %v", funcString, script) + } + } + }) + } +} diff --git a/internal/controller/blobdownload/gpkg_download.sh b/internal/controller/blobdownload/gpkg_download.sh new file mode 100644 index 0000000..53a43a7 --- /dev/null +++ b/internal/controller/blobdownload/gpkg_download.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash + +set -euo pipefail + +function download_gpkg() { + local gpkg=$1 + local file=$2 + local url=$3 + + if [ -f "$file" ] && [ ! -f "$file".st ]; then + echo msg=\"File already downloaded\" file=\""$file"\" + else + echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" + + # use curl to check if resource exists + # axel blocks on non-existing resources + curl -IfsS "$url" > /dev/null + + echo start "$gpkg" + ret=0 + # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. + axel -n 1 -T 120 -o "$file" "$url" \ + | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ + | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? + + if [ $ret -ne 0 ] + then + echo -e '\n' + # Download failed ($? != 0). + if [ $ret -eq 1 ] + then + # Axel was not able to resume ($? == 1). Remove file and state file. + if [ -f "$file" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file"\" + rm "$file" + fi + if [ -f "$file.st" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file".st\" + rm "$file".st + fi + else + # Download failed with other error ($? > 1). Remove file if state file does not exist. + if [ ! -f "$file.st" ]; then + echo msg=\"Download failed without state file, removing file\" file=\""$file"\" + rm "$file" + fi + fi + + # Retry the download + echo msg=\"Retry file\" file=\""$file"\" + download_gpkg $gpkg $file $url + fi + fi +} + +function download() { + if [ -z "$BLOBS_ENDPOINT" ]; + then + echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; + exit 1; + fi + + local gpkg=$1 + local file=$GEOPACKAGE_TARGET_PATH/$2 + local url=${BLOBS_ENDPOINT}/${gpkg} + + download_gpkg $gpkg $file $url + + # Check Content-length + download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') + file_size=$(wc -c "$file" | awk '{print $1}') + if [ "$download_size" != "$file_size" ] + then + echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + rm_file_and_exit + else + echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + chown 999:999 "$file" + fi + + # Check ogrinfo + echo "Check gpkg with ogrinfo" + if ! ogrinfo -so "$file" + then + echo "ERROR: ogrinfo check on $file failed" + rm_file_and_exit + fi + + # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) + echo "Check if md5 hash value exists in blob storage" + rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" + + # If file contains valid hash, then check it, else skip + hash=$(awk '{ print $1 }' "${file}.md5sum-remote") + if [[ $hash =~ ^[a-f0-9]{32}$ ]] + then + echo "Valid hash value found" + echo "Compare MD5 hash of remote and downloaded gpkg" + if ! (echo "$hash $file" | md5sum --check); then + rm_file_and_exit + fi + else + echo "No hash found for $file in blob storage, skipping checksum." + fi + + # After successful download set the GPKG to readonly + chmod -wx $file + echo "done" +} + +function download_all() { + echo msg=\"Starting GeoPackage downloader\" + + local start_time=$(date '+%s') + + # create target location if not exists + mkdir -p $GEOPACKAGE_TARGET_PATH + chown 999:999 $GEOPACKAGE_TARGET_PATH + + # Download all geopackages from GEOPACKAGE_DOWNLOAD_LIST + # Example: GEOPACKAGE_DOWNLOAD_LIST=path/1/file.gpkg;path/3/other_file.gpkg + gpkgs=(${GEOPACKAGE_DOWNLOAD_LIST//;/ }) + for gpkg_path in "${gpkgs[@]}" + do + filename=$(basename $gpkg_path) + download $gpkg_path $filename + done + + echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) +} + +function rm_file_and_exit() { + echo "Removing $file, to ensure a fresh new download is started when script is executed again" + rm -rf "$file" + + if [ -f "$file.st" ]; then + rm "$file".st + fi + + echo "Exiting..." + exit 1 +} + +download_all | awk -W interactive ' +BEGIN { + state="idle"; +} + +{ + if ($0 != "") { + if ($1 == "start") { + gpkg=$2; + state="downloading"; + } else if ($1 == "done") { + state="idle"; + } else if (state == "downloading") { + if ($1 == "progress") { + # reduce output to prevent loki from choking on large log volume + if (last_percentage != $2) { + if ($3 == "") { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; + } else { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; + } + } + last_percentage=$2; + } else { + print "msg=\"" $0 "\" gpkg=" gpkg; + } + } else { + print $0; + } + } +} +' diff --git a/internal/controller/capabilitiesgenerator/capabilities_generator.go b/internal/controller/capabilitiesgenerator/capabilities_generator.go new file mode 100644 index 0000000..68fd51f --- /dev/null +++ b/internal/controller/capabilitiesgenerator/capabilities_generator.go @@ -0,0 +1,76 @@ +package capabilitiesgenerator + +import ( + "fmt" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + "github.com/pdok/mapserver-operator/internal/controller/types" + "github.com/pdok/mapserver-operator/internal/controller/utils" + "gopkg.in/yaml.v3" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + corev1 "k8s.io/api/core/v1" +) + +func GetCapabilitiesGeneratorInitContainer[O pdoknlv3.WMSWFS](_ O, images types.Images) (*corev1.Container, error) { + initContainer := corev1.Container{ + Name: constants.CapabilitiesGeneratorName, + Image: images.CapabilitiesGeneratorImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{ + { + Name: "SERVICECONFIG", + Value: "/input/input.yaml", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + utils.GetDataVolumeMount(), + utils.GetConfigVolumeMount(constants.ConfigMapCapabilitiesGeneratorVolumeName), + }, + } + return &initContainer, nil +} + +func GetInput[W pdoknlv3.WMSWFS](webservice W, ownerInfo *smoothoperatorv1.OwnerInfo) (input string, err error) { + switch any(webservice).(type) { + case *pdoknlv3.WFS: + if WFS, ok := any(webservice).(*pdoknlv3.WFS); ok { + return createInputForWFS(WFS, ownerInfo) + } + case *pdoknlv3.WMS: + if WMS, ok := any(webservice).(*pdoknlv3.WMS); ok { + return createInputForWMS(WMS, ownerInfo) + } + default: + return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) + } + return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) +} + +func createInputForWFS(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { + input, err := MapWFSToCapabilitiesGeneratorInput(wfs, ownerInfo) + if err != nil { + return "", err + } + yamlInput, err := yaml.Marshal(input) + if err != nil { + return "", fmt.Errorf("failed to marshal the capabilities generator input to yaml: %w", err) + } + + return string(yamlInput), nil +} + +func createInputForWMS(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { + input, err := MapWMSToCapabilitiesGeneratorInput(wms, ownerInfo) + if err != nil { + return "", err + } + yamlInput, err := yaml.Marshal(input) + if err != nil { + return "", fmt.Errorf("failed to marshal the capabilities generator input to yaml: %w", err) + } + + return string(yamlInput), nil +} diff --git a/internal/controller/capabilitiesgenerator/capabilities_generator_test.go b/internal/controller/capabilitiesgenerator/capabilities_generator_test.go new file mode 100644 index 0000000..cda5991 --- /dev/null +++ b/internal/controller/capabilitiesgenerator/capabilities_generator_test.go @@ -0,0 +1,220 @@ +package capabilitiesgenerator + +import ( + "github.com/google/go-cmp/cmp" + "github.com/pdok/mapserver-operator/api/v2beta1" + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + capabilitiesgenerator "github.com/pdok/ogc-capabilities-generator/pkg/config" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatormodel "github.com/pdok/smooth-operator/model" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + yamlv3 "sigs.k8s.io/yaml/goyaml.v3" + + "testing" + + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + + _ "embed" +) + +//go:embed test_data/wfs_input.yaml +var WFSInput string + +//go:embed test_data/wms_input.yaml +var WMSInput string + +func TestGetInputForWFS(t *testing.T) { + type args struct { + WFS *pdoknlv3.WFS + ownerInfo *smoothoperatorv1.OwnerInfo + } + url, _ := smoothoperatormodel.ParseURL("http://localhost/datasetOwner/dataset/theme/wfs/v1_0") + pdoknlv3.SetHost("http://localhost") + accessConstraints, _ := smoothoperatormodel.ParseURL("http://creativecommons.org/publicdomain/zero/1.0/deed.nl") + tests := []struct { + name string + args args + wantInput string + wantErr bool + }{ + { + name: "GetInputForWFS", + args: args{ + WFS: &pdoknlv3.WFS{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "dataset": "dataset", + "dataset-owner": "datasetOwner", + "theme": "theme", + "service-version": "v1_0", + }, + }, + Spec: pdoknlv3.WFSSpec{ + Service: pdoknlv3.WFSService{BaseService: pdoknlv3.BaseService{ + URL: smoothoperatormodel.URL{URL: url}, + Prefix: "prefix", + Title: "some Service title", + Abstract: "some \"Service\" abstract", + Keywords: []string{"service-keyword-1", "service-keyword-2", "infoFeatureAccessService"}, + AccessConstraints: smoothoperatormodel.URL{URL: accessConstraints}}, + Inspire: &pdoknlv3.WFSInspire{Inspire: pdoknlv3.Inspire{ + ServiceMetadataURL: pdoknlv3.MetadataURL{ + CSW: &pdoknlv3.Metadata{ + MetadataIdentifier: "metameta-meta-meta-meta-metametameta", + }, + }, + Language: "dut"}, + SpatialDatasetIdentifier: "datadata-data-data-data-datadatadata", + }, + DefaultCrs: "EPSG:28992", + OtherCrs: []string{ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + }, + FeatureTypes: []pdoknlv3.FeatureType{ + { + Name: "featuretype-1-name", + Title: "featuretype-1-title", + Abstract: "feature \"1\" abstract", + Keywords: []string{"featuretype-1-keyword-1", "featuretype-1-keyword-2"}, + DatasetMetadataURL: &pdoknlv3.MetadataURL{ + CSW: &pdoknlv3.Metadata{ + MetadataIdentifier: "datadata-data-data-data-datadatadata", + }, + }, + Bbox: &pdoknlv3.FeatureBbox{ + WGS84: &smoothoperatormodel.BBox{ + MinX: "-180", + MaxX: "180", + MinY: "-90", + MaxY: "90", + }, + }, + }, + { + Name: "featuretype-2-name", + Title: "featuretype-2-title", + Abstract: "feature \"2\" abstract", + Keywords: []string{"featuretype-2-keyword-1", "featuretype-2-keyword-2"}, + DatasetMetadataURL: &pdoknlv3.MetadataURL{ + CSW: &pdoknlv3.Metadata{ + MetadataIdentifier: "datadata-data-data-data-datadatadata", + }, + }, + }, + }, + }, + }, + }, + ownerInfo: &smoothoperatorv1.OwnerInfo{ + Spec: smoothoperatorv1.OwnerInfoSpec{ + NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), + MetadataUrls: &smoothoperatorv1.MetadataUrls{ + CSW: &smoothoperatorv1.MetadataURL{ + HrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}", + }, + }, + WFS: &smoothoperatorv1.WFS{ + ServiceProvider: smoothoperatorv1.ServiceProvider{ + ProviderName: smoothoperatorutils.Pointer("PDOK"), + }, + }, + }, + }, + }, + wantInput: WFSInput, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotInput, err := GetInput(tt.args.WFS, tt.args.ownerInfo) + if (err != nil) != tt.wantErr { + t.Errorf("GetInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + + wantMap := capabilitiesgenerator.Config{} + gotMap := capabilitiesgenerator.Config{} + err = yamlv3.Unmarshal([]byte(WFSInput), &wantMap) + assert.NoError(t, err) + err = yamlv3.Unmarshal([]byte(gotInput), &gotMap) + assert.NoError(t, err) + + diff := cmp.Diff(wantMap, gotMap) + if diff != "" { + t.Errorf("GetInput() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestInputForWMS(t *testing.T) { + //nolint:misspell + v2wmsstring := "apiVersion: pdok.nl/v2beta1\nkind: WMS\nmetadata:\n name: rws-nwbwegen-v1-0\n labels:\n dataset-owner: rws\n dataset: nwbwegen\n service-version: v1_0\n service-type: wms\n annotations:\n lifecycle-phase: prod\n service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb\nspec:\n general:\n datasetOwner: rws\n dataset: nwbwegen\n serviceVersion: v1_0\n kubernetes:\n healthCheck:\n boundingbox: 135134.89,457152.55,135416.03,457187.82\n resources:\n limits:\n ephemeralStorage: 1535Mi\n memory: 4G\n requests:\n cpu: 2000m\n ephemeralStorage: 1535Mi\n memory: 4G\n options:\n automaticCasing: true\n disableWebserviceProxy: false\n includeIngress: true\n validateRequests: true\n service:\n title: NWB - Wegen WMS\n abstract:\n Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen.\n Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen\n Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland.\n Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk,\n provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien\n van een straatnaam of nummer.\n authority:\n name: rws\n url: https://www.rijkswaterstaat.nl\n dataEPSG: EPSG:28992\n extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961\n inspire: true\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n - Hectometerpunten\n - HVD\n - Mobiliteit\n stylingAssets:\n configMapRefs:\n - name: includes\n keys:\n - nwb_wegen_hectopunten.symbol\n - hectopunten.style\n - wegvakken.style\n blobKeys:\n - resources/fonts/liberation-sans.ttf\n layers:\n - abstract:\n Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB)\n en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer,\n routenummer, wegbeheerder, huisnummers, enz. weer.\n data:\n gpkg:\n columns:\n - objectid\n - wvk_id\n - wvk_begdat\n - jte_id_beg\n - jte_id_end\n - wegbehsrt\n - wegnummer\n - wegdeelltr\n - hecto_lttr\n - bst_code\n - rpe_code\n - admrichtng\n - rijrichtng\n - stt_naam\n - stt_bron\n - wpsnaam\n - gme_id\n - gme_naam\n - hnrstrlnks\n - hnrstrrhts\n - e_hnr_lnks\n - e_hnr_rhts\n - l_hnr_lnks\n - l_hnr_rhts\n - begafstand\n - endafstand\n - beginkm\n - eindkm\n - pos_tv_wol\n - wegbehcode\n - wegbehnaam\n - distrcode\n - distrnaam\n - dienstcode\n - dienstnaam\n - wegtype\n - wgtype_oms\n - routeltr\n - routenr\n - routeltr2\n - routenr2\n - routeltr3\n - routenr3\n - routeltr4\n - routenr4\n - wegnr_aw\n - wegnr_hmp\n - geobron_id\n - geobron_nm\n - bronjaar\n - openlr\n - bag_orl\n - frc\n - fow\n - alt_naam\n - alt_nr\n - rel_hoogte\n - st_lengthshape\n geometryType: MultiLineString\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: wegvakken\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Wegvakken\n maxScale: 50000.0\n minScale: 1.0\n name: wegvakken\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: wegvakken\n title: NWB - Wegvakken\n visualization: wegvakken.style\n title: Wegvakken\n visible: true\n - abstract:\n Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB)\n en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand,\n zijde en hectoletter weer.\n data:\n gpkg:\n columns:\n - objectid\n - hectomtrng\n - afstand\n - wvk_id\n - wvk_begdat\n - zijde\n - hecto_lttr\n geometryType: MultiPoint\n blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg\n table: hectopunten\n datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502\n keywords:\n - Vervoersnetwerken\n - Menselijke gezondheid en veiligheid\n - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai)\n - Nationaal\n - Voertuigen\n - Verkeer\n - Hectometerpunten\n maxScale: 50000.0\n minScale: 1.0\n name: hectopunten\n sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff\n styles:\n - name: hectopunten\n title: NWB - Hectopunten\n visualization: hectopunten.style\n title: Hectopunten\n visible: true\n metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8\n" + v2wms := &v2beta1.WMS{} + err := yaml.Unmarshal([]byte(v2wmsstring), v2wms) + assert.NoError(t, err) + pdoknlv3.SetHost("http://localhost") + var wms pdoknlv3.WMS + err = v2wms.ToV3(&wms) + assert.NoError(t, err) + + contactPersonPrimary := smoothoperatorv1.ContactPersonPrimary{ + ContactPerson: smoothoperatorutils.Pointer("KlantContactCenter PDOK"), + ContactOrganization: smoothoperatorutils.Pointer("PDOK"), + } + + ownerInfo := smoothoperatorv1.OwnerInfo{ + Spec: smoothoperatorv1.OwnerInfoSpec{ + NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), + MetadataUrls: &smoothoperatorv1.MetadataUrls{ + CSW: &smoothoperatorv1.MetadataURL{ + HrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}", + }, + }, + ProviderSite: &smoothoperatorv1.ProviderSite{ + Type: "simple", + Href: "https://www.pdok.nl", + }, + WMS: &smoothoperatorv1.WMS{ + ContactInformation: smoothoperatorv1.ContactInformation{ + ContactPersonPrimary: &contactPersonPrimary, + ContactPosition: smoothoperatorutils.Pointer("pointOfContact"), + ContactAddress: &smoothoperatorv1.ContactAddress{ + AddressType: smoothoperatorutils.Pointer("Work"), + Address: nil, + City: smoothoperatorutils.Pointer("Apeldoorn"), + StateOrProvince: nil, + PostCode: nil, + Country: smoothoperatorutils.Pointer("The Netherlands"), + }, + ContactVoiceTelephone: nil, + ContactFacsimileTelephone: nil, + ContactElectronicMailAddress: smoothoperatorutils.Pointer("BeheerPDOK@kadaster.nl"), + }, + }, + }, + } + + input, err := GetInput(&wms, &ownerInfo) + assert.NoError(t, err) + + wantMap := capabilitiesgenerator.Config{} + gotMap := capabilitiesgenerator.Config{} + err = yamlv3.Unmarshal([]byte(WMSInput), &wantMap) + assert.NoError(t, err) + err = yamlv3.Unmarshal([]byte(input), &gotMap) + assert.NoError(t, err) + + diff := cmp.Diff(wantMap, gotMap) + assert.Equal(t, diff, "", "%s", diff) +} diff --git a/internal/controller/capabilitiesgenerator/default_bboxes.go b/internal/controller/capabilitiesgenerator/default_bboxes.go new file mode 100644 index 0000000..08803ef --- /dev/null +++ b/internal/controller/capabilitiesgenerator/default_bboxes.go @@ -0,0 +1,132 @@ +package capabilitiesgenerator + +import "github.com/pdok/ogc-specifications/pkg/wms130" + +// TODO Bounding boxes and default CRSes in this file are not used at the moment but are kept so we can use them again later + +//nolint:unused +var defaultWMSBoundingBox = wms130.EXGeographicBoundingBox{ + WestBoundLongitude: 2.52713, + EastBoundLongitude: 7.37403, + SouthBoundLatitude: 50.2129, + NorthBoundLatitude: 55.7212, +} + +//nolint:unused +func getDefaultWMSCRSes() []wms130.CRS { + return []wms130.CRS{{ + Namespace: "EPSG", + Code: 28992, + }, { + Namespace: "EPSG", + Code: 25831, + }, { + Namespace: "EPSG", + Code: 25832, + }, { + Namespace: "EPSG", + Code: 3034, + }, { + Namespace: "EPSG", + Code: 3035, + }, { + Namespace: "EPSG", + Code: 3857, + }, { + Namespace: "EPSG", + Code: 4258, + }, { + Namespace: "EPSG", + Code: 4326, + }, { + Namespace: "CRS", + Code: 84, + }} +} + +//nolint:unused +func getDefaultWMSLayerBoundingBoxes() []*wms130.LayerBoundingBox { + return []*wms130.LayerBoundingBox{ + { + CRS: "EPSG:28992", + Minx: -25000, + Miny: 250000, + Maxx: 280000, + Maxy: 860000, + Resx: 0, + Resy: 0, + }, + { + CRS: "EPSG:25831", + Minx: -470271, + Miny: 5.56231e+06, + Maxx: 795163, + Maxy: 6.18197e+06, + Resx: 0, + Resy: 0, + }, + { + CRS: "EPSG:25832", + Minx: 62461.6, + Miny: 5.56555e+06, + Maxx: 397827, + Maxy: 6.19042e+06, + Resx: 0, + Resy: 0, + }, + { + CRS: "EPSG:3034", + Minx: 2.61336e+06, + Miny: 3.509e+06, + Maxx: 3.22007e+06, + Maxy: 3.84003e+06, + Resx: 0, + Resy: 0, + }, + { + CRS: "EPSG:3035", + Minx: 3.01676e+06, + Miny: 3.81264e+06, + Maxx: 3.64485e+06, + Maxy: 4.15586e+06, + Resx: 0, + Resy: 0, + }, + { + CRS: "EPSG:3857", + Minx: 281318, + Miny: 6.48322e+06, + Maxx: 820873, + Maxy: 7.50311e+06, + Resx: 0, + Resy: 0, + }, + { + CRS: "EPSG:4258", + Minx: 50.2129, + Miny: 2.52713, + Maxx: 55.7212, + Maxy: 7.37403, + Resx: 0, + Resy: 0, + }, + { + CRS: "EPSG:4326", + Minx: 50.2129, + Miny: 2.52713, + Maxx: 55.7212, + Maxy: 7.37403, + Resx: 0, + Resy: 0, + }, + { + CRS: "CRS:84", + Minx: 2.52713, + Miny: 50.2129, + Maxx: 7.37403, + Maxy: 55.7212, + Resx: 0, + Resy: 0, + }, + } +} diff --git a/internal/controller/capabilitiesgenerator/mapper.go b/internal/controller/capabilitiesgenerator/mapper.go new file mode 100644 index 0000000..7a3e8d9 --- /dev/null +++ b/internal/controller/capabilitiesgenerator/mapper.go @@ -0,0 +1,691 @@ +package capabilitiesgenerator + +import ( + "fmt" + "slices" + "strconv" + "strings" + + "k8s.io/utils/ptr" + + "github.com/pdok/ogc-specifications/pkg/wms130" + + "github.com/cbroglie/mustache" + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/mapperutils" + capabilitiesgenerator "github.com/pdok/ogc-capabilities-generator/pkg/config" + "github.com/pdok/ogc-specifications/pkg/wfs200" + "github.com/pdok/ogc-specifications/pkg/wsc110" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +const ( + inspireSchemaLocationsWFS = "http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd" + inspireSchemaLocationsWMS = "http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd" + wfsCapabilitiesFilename = "/var/www/config/capabilities_wfs_200.xml" + wmsCapabilitiesFilename = "/var/www/config/capabilities_wms_130.xml" + metadataMediaType = "application/vnd.ogc.csw.GetRecordByIdResponse_xml" + XLinkURL = "http://www.w3.org/1999/xlink" +) + +func MapWFSToCapabilitiesGeneratorInput(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (*capabilitiesgenerator.Config, error) { + featureTypeList, err := getFeatureTypeList(wfs, ownerInfo) + if err != nil { + return nil, err + } + + config := capabilitiesgenerator.Config{ + Global: capabilitiesgenerator.Global{ + Namespace: mapperutils.GetNamespaceURI(wfs.Spec.Service.Prefix, ownerInfo), + Prefix: wfs.Spec.Service.Prefix, + Onlineresourceurl: wfs.URL().Scheme + "://" + wfs.URL().Host, + Path: wfs.URL().Path, + }, + Services: capabilitiesgenerator.Services{ + WFS200Config: &capabilitiesgenerator.WFS200Config{ + Filename: wfsCapabilitiesFilename, + Wfs200: wfs200.GetCapabilitiesResponse{ + + ServiceProvider: mapServiceProvider(&ownerInfo.Spec.WFS.ServiceProvider, ownerInfo.Spec.ProviderSite), + ServiceIdentification: wfs200.ServiceIdentification{ + Title: wfs.Spec.Service.Title, + Abstract: wfs.Spec.Service.Abstract, + AccessConstraints: wfs.Spec.Service.AccessConstraints.String(), + Keywords: &wsc110.Keywords{ + Keyword: wfs.Spec.Service.KeywordsIncludingInspireKeyword(), + }, + Fees: wfs.Spec.Service.Fees, + }, + Capabilities: wfs200.Capabilities{ + FeatureTypeList: *featureTypeList, + }, + }, + }, + }, + } + + if wfs.Spec.Service.Inspire != nil { + config.Global.AdditionalSchemaLocations = inspireSchemaLocationsWFS + var metadataURL wfs200.MetadataURL + if wfs.Spec.Service.Inspire.ServiceMetadataURL.CSW != nil { + metadataURL.URL, err = replaceMustacheTemplate(ownerInfo.Spec.MetadataUrls.CSW.HrefTemplate, wfs.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier) + if err != nil { + return nil, err + } + metadataURL.MediaType = metadataMediaType + } + + if wfs.Spec.Service.Inspire.ServiceMetadataURL.Custom != nil { + metadataURL.URL = wfs.Spec.Service.Inspire.ServiceMetadataURL.Custom.Href.String() + metadataURL.MediaType = wfs.Spec.Service.Inspire.ServiceMetadataURL.Custom.Type + } + + config.Services.WFS200Config.Wfs200.Capabilities.OperationsMetadata = &wfs200.OperationsMetadata{ + ExtendedCapabilities: &wfs200.ExtendedCapabilities{ + ExtendedCapabilities: wfs200.NestedExtendedCapabilities{ + MetadataURL: metadataURL, + SupportedLanguages: wfs200.SupportedLanguages{ + DefaultLanguage: wfs200.Language{ + Language: wfs.Spec.Service.Inspire.Language, + }, + }, + ResponseLanguage: wfs200.Language{Language: wfs.Spec.Service.Inspire.Language}, + SpatialDataSetIdentifier: wfs200.SpatialDataSetIdentifier{ + Code: wfs.Spec.Service.Inspire.SpatialDatasetIdentifier, + }, + }, + }, + } + } + if wfs.Spec.Service.CountDefault != nil { + operationsMetadata := config.Services.WFS200Config.Wfs200.Capabilities.OperationsMetadata + if operationsMetadata == nil { + operationsMetadata = &wfs200.OperationsMetadata{} + } + operationsMetadata.Constraint = getConstraints(strconv.Itoa(*wfs.Spec.Service.CountDefault)) + config.Services.WFS200Config.Wfs200.Capabilities.OperationsMetadata = operationsMetadata + } + + return &config, nil +} + +func getConstraints(countDefault string) []wfs200.Constraint { + return []wfs200.Constraint{ + { + Name: "ImplementsBasicWFS", + DefaultValue: ptr.To("TRUE"), + }, + { + Name: "ImplementsTransactionalWFS", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ImplementsLockingWFS", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "KVPEncoding", + DefaultValue: ptr.To("TRUE"), + }, + { + Name: "XMLEncoding", + DefaultValue: ptr.To("TRUE"), + }, + { + Name: "SOAPEncoding", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ImplementsInheritance", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ImplementsRemoteResolve", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ImplementsResultPaging", + DefaultValue: ptr.To("TRUE"), + }, + { + Name: "ImplementsStandardJoins", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ImplementsSpatialJoins", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ImplementsTemporalJoins", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ImplementsFeatureVersioning", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "ManageStoredQueries", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "PagingIsTransactionSafe", + DefaultValue: ptr.To("FALSE"), + }, + { + Name: "CountDefault", + DefaultValue: &countDefault, + }, + { + Name: "QueryExpressions", + AllowedValues: &wfs200.AllowedValues{Value: []string{ + "wfs:Query", + "wfs:StoredQuery", + }, + }, + }, + } +} + +func getFeatureTypeList(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (*wfs200.FeatureTypeList, error) { + typeList := wfs200.FeatureTypeList{} + + for _, fType := range wfs.Spec.Service.FeatureTypes { + defaultCRS, err := createCRSFromEpsgString(wfs.Spec.Service.DefaultCrs) + if err != nil { + return nil, err + } + + var otherCRS []*wfs200.CRS + for _, epsgString := range wfs.Spec.Service.OtherCrs { + CRS, err := createCRSFromEpsgString(epsgString) + if err != nil { + return nil, err + } + otherCRS = append(otherCRS, CRS) + } + + var wgs84BoundingBox *wsc110.WGS84BoundingBox + if fType.Bbox != nil && fType.Bbox.WGS84 != nil { + minX, err := strconv.ParseFloat(fType.Bbox.WGS84.MinX, 64) + if err != nil { + return nil, err + } + maxX, err := strconv.ParseFloat(fType.Bbox.WGS84.MaxX, 64) + if err != nil { + return nil, err + } + minY, err := strconv.ParseFloat(fType.Bbox.WGS84.MinY, 64) + if err != nil { + return nil, err + } + maxY, err := strconv.ParseFloat(fType.Bbox.WGS84.MaxY, 64) + if err != nil { + return nil, err + } + + wgs84BoundingBox = &wsc110.WGS84BoundingBox{ + LowerCorner: wsc110.Position{minX, minY}, + UpperCorner: wsc110.Position{maxX, maxY}, + } + } + + metadataURL, err := replaceMustacheTemplate(ownerInfo.Spec.MetadataUrls.CSW.HrefTemplate, fType.DatasetMetadataURL.CSW.MetadataIdentifier) + if err != nil { + return nil, err + } + + featureType := wfs200.FeatureType{ + Name: wfs.Spec.Service.Prefix + ":" + fType.Name, + Title: fType.Title, + Abstract: fType.Abstract, + Keywords: &[]wsc110.Keywords{ + { + Keyword: fType.Keywords, + }, + }, + MetadataURL: wfs200.MetadataHref{ + Href: metadataURL, + }, + DefaultCRS: defaultCRS, + OtherCRS: otherCRS, + WGS84BoundingBox: wgs84BoundingBox, + } + + typeList.FeatureType = append(typeList.FeatureType, featureType) + } + return &typeList, nil +} + +func createCRSFromEpsgString(epsgString string) (*wfs200.CRS, error) { + index := strings.LastIndex(epsgString, ":") + if index == -1 { + return nil, fmt.Errorf("could not determine EPSG code from EPSG string %s", epsgString) + } + epsgCodeString := epsgString[index+1:] + epsgCode, err := strconv.Atoi(epsgCodeString) + if err != nil { + return nil, fmt.Errorf("could not determine EPSG code from EPSG string %s", epsgCodeString) + } + + epsgUrn := "urn:ogc:def:crs:EPSG:" + + return &wfs200.CRS{ + Namespace: epsgUrn, + Code: epsgCode, + }, nil +} + +func replaceMustacheTemplate(hrefTemplate string, identifier string) (string, error) { + templateVariable := map[string]string{"identifier": identifier} + return mustache.Render(hrefTemplate, templateVariable) +} + +func mapServiceProvider(provider *smoothoperatorv1.ServiceProvider, providerSite *smoothoperatorv1.ProviderSite) (serviceProvider wfs200.ServiceProvider) { + if provider.ProviderName != nil { + serviceProvider.ProviderName = provider.ProviderName + } + + if providerSite != nil { + serviceProvider.ProviderSite = &wfs200.ProviderSite{ + Type: providerSite.Type, + Href: providerSite.Href, + } + } + + if provider.ServiceContact != nil { + serviceProvider.ServiceContact = &wfs200.ServiceContact{ + IndividualName: provider.ServiceContact.IndividualName, + PositionName: provider.ServiceContact.PositionName, + Role: provider.ServiceContact.Role, + } + if provider.ServiceContact.ContactInfo != nil { + serviceProvider.ServiceContact.ContactInfo = mapContactInfo(*provider.ServiceContact.ContactInfo) + } + } + + return serviceProvider +} + +func mapContactInfo(contactInfo smoothoperatorv1.ContactInfo) (serviceContactInfo *wfs200.ContactInfo) { + serviceContactInfo = &wfs200.ContactInfo{ + Text: contactInfo.Text, + HoursOfService: contactInfo.HoursOfService, + ContactInstructions: contactInfo.ContactInstructions, + } + if contactInfo.Phone != nil { + serviceContactInfo.Phone = &wfs200.Phone{ + Voice: contactInfo.Phone.Voice, + Facsimile: contactInfo.Phone.Facsimile, + } + } + if contactInfo.Address != nil { + serviceContactInfo.Address = &wfs200.Address{ + DeliveryPoint: contactInfo.Address.DeliveryPoint, + City: contactInfo.Address.City, + AdministrativeArea: contactInfo.Address.AdministrativeArea, + PostalCode: contactInfo.Address.PostalCode, + Country: contactInfo.Address.Country, + ElectronicMailAddress: contactInfo.Address.ElectronicMailAddress, + } + } + if contactInfo.OnlineResource != nil { + serviceContactInfo.OnlineResource = &wfs200.OnlineResource{ + Type: contactInfo.OnlineResource.Type, + Href: contactInfo.OnlineResource.Href, + } + } + return +} + +func MapWMSToCapabilitiesGeneratorInput(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (*capabilitiesgenerator.Config, error) { + canonicalServiceURL := wms.URL() + + layer, err := getLayers(wms, canonicalServiceURL.String()) + if err != nil { + return nil, err + } + + config := capabilitiesgenerator.Config{ + Global: capabilitiesgenerator.Global{ + // Prefix is unused for the WMS, but doesn't hurt to pass it + Namespace: mapperutils.GetNamespaceURI(wms.Spec.Service.Prefix, ownerInfo), + Prefix: wms.Spec.Service.Prefix, + Onlineresourceurl: wms.URL().Scheme + "://" + wms.URL().Host, + Path: wms.URL().Path, + }, + Services: capabilitiesgenerator.Services{ + WMS130Config: &capabilitiesgenerator.WMS130Config{ + Filename: wmsCapabilitiesFilename, + Wms130: wms130.GetCapabilitiesResponse{ + WMSService: wms130.WMSService{ + Name: "WMS", + Title: wms.Spec.Service.Title, + Abstract: &wms.Spec.Service.Abstract, + KeywordList: &wms130.Keywords{Keyword: wms.Spec.Service.KeywordsIncludingInspireKeyword()}, + OnlineResource: wms130.OnlineResource{Href: &ownerInfo.Spec.ProviderSite.Href}, + ContactInformation: getContactInformation(ownerInfo), + Fees: wms.Spec.Service.Fees, + AccessConstraints: ptr.To(wms.Spec.Service.AccessConstraints.String()), + OptionalConstraints: &wms130.OptionalConstraints{ + MaxWidth: int(smoothoperatorutils.PointerVal(wms.Spec.Service.MaxSize, 4000)), + MaxHeight: int(smoothoperatorutils.PointerVal(wms.Spec.Service.MaxSize, 4000)), + }, + }, + Capabilities: wms130.Capabilities{ + WMSCapabilities: wms130.WMSCapabilities{ + Request: wms130.Request{ + GetCapabilities: wms130.RequestType{ + Format: []string{"text/xml"}, + DCPType: getDcpType(canonicalServiceURL.String(), false), + }, + GetMap: wms130.RequestType{ + Format: []string{"image/png", "image/jpeg", "image/png; mode=8bit", "image/vnd.jpeg-png", "image/vnd.jpeg-png8"}, + DCPType: getDcpType(canonicalServiceURL.String(), true), + }, + GetFeatureInfo: &wms130.RequestType{ + Format: []string{"application/json", "application/json; subtype=geojson", "application/vnd.ogc.gml", "text/html", "text/plain", "text/xml", "text/xml; subtype=gml/3.1.1"}, + DCPType: getDcpType(canonicalServiceURL.String(), true), + }, + }, + Exception: wms130.ExceptionType{Format: []string{"XML", "INIMAGE", "BLANK"}}, + ExtendedCapabilities: nil, + Layer: layer, + }, + OptionalConstraints: nil, + }, + }, + }, + }, + } + + if wms.Spec.Service.Inspire != nil { + config.Global.AdditionalSchemaLocations = inspireSchemaLocationsWMS + metadataURL, _ := replaceMustacheTemplate(ownerInfo.Spec.MetadataUrls.CSW.HrefTemplate, wms.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier) + + defaultLanguage := wms130.Language{Language: wms.Spec.Service.Inspire.Language} + + config.Services.WMS130Config.Wms130.Capabilities.ExtendedCapabilities = &wms130.ExtendedCapabilities{ + MetadataURL: wms130.ExtendedMetadataURL{URL: metadataURL, MediaType: metadataMediaType}, + SupportedLanguages: wms130.SupportedLanguages{ + DefaultLanguage: defaultLanguage, + SupportedLanguage: &[]wms130.Language{defaultLanguage}, + }, + ResponseLanguage: defaultLanguage, + } + } + + return &config, nil +} + +func getContactInformation(ownerInfo *smoothoperatorv1.OwnerInfo) *wms130.ContactInformation { + result := wms130.ContactInformation{ + ContactPersonPrimary: nil, + ContactPosition: nil, + ContactAddress: nil, + ContactVoiceTelephone: nil, + ContactFacsimileTelephone: nil, + ContactElectronicMailAddress: nil, + } + + providedContactInformation := ownerInfo.Spec.WMS.ContactInformation + if providedContactInformation.ContactPersonPrimary != nil { + contactPerson := "" + if providedContactInformation.ContactPersonPrimary.ContactPerson != nil { + contactPerson = *providedContactInformation.ContactPersonPrimary.ContactPerson + } + contactOrganisation := "" + if providedContactInformation.ContactPersonPrimary.ContactOrganization != nil { + contactOrganisation = *providedContactInformation.ContactPersonPrimary.ContactOrganization + } + + contactPersonPrimary := wms130.ContactPersonPrimary{ + ContactPerson: contactPerson, + ContactOrganization: contactOrganisation, + } + result.ContactPersonPrimary = &contactPersonPrimary + } + + result.ContactPosition = providedContactInformation.ContactPosition + if providedContactInformation.ContactAddress != nil { + contactAddressInput := providedContactInformation.ContactAddress + contactAddress := wms130.ContactAddress{ + AddressType: smoothoperatorutils.PointerVal(contactAddressInput.AddressType, ""), + Address: smoothoperatorutils.PointerVal(contactAddressInput.Address, ""), + City: smoothoperatorutils.PointerVal(contactAddressInput.City, ""), + StateOrProvince: smoothoperatorutils.PointerVal(contactAddressInput.StateOrProvince, ""), + PostalCode: smoothoperatorutils.PointerVal(contactAddressInput.PostCode, ""), + Country: smoothoperatorutils.PointerVal(contactAddressInput.Country, ""), + } + result.ContactAddress = &contactAddress + } + + result.ContactVoiceTelephone = providedContactInformation.ContactVoiceTelephone + result.ContactFacsimileTelephone = providedContactInformation.ContactFacsimileTelephone + result.ContactElectronicMailAddress = providedContactInformation.ContactElectronicMailAddress + + return &result +} + +func getDcpType(url string, fillPost bool) *wms130.DCPType { + get := wms130.Method{ + OnlineResource: wms130.OnlineResource{ + Xlink: smoothoperatorutils.Pointer(XLinkURL), + Type: nil, + Href: smoothoperatorutils.Pointer(url + "?"), + }, + } + + var post *wms130.Method + if fillPost { + post = &wms130.Method{ + OnlineResource: wms130.OnlineResource{ + Xlink: smoothoperatorutils.Pointer(XLinkURL), + Type: nil, + Href: smoothoperatorutils.Pointer(url), + }, + } + } + + result := wms130.DCPType{ + HTTP: struct { + Get wms130.Method `xml:"Get" yaml:"get"` + Post *wms130.Method `xml:"Post" yaml:"post"` + }{ + Get: get, + Post: post, + }, + } + return &result +} + +func getLayers(wms *pdoknlv3.WMS, canonicalURL string) ([]wms130.Layer, error) { + layer, err := mapLayer(wms.Spec.Service.Layer, canonicalURL, nil, nil, nil, nil) + if err != nil { + return nil, err + } + return []wms130.Layer{*layer}, nil +} + +func mapLayer(layer pdoknlv3.Layer, canonicalURL string, authorityURL *wms130.AuthorityURL, identifier *wms130.Identifier, parentStyleNames []string, parentBBoxes []*wms130.LayerBoundingBox) (*wms130.Layer, error) { + if layer.Authority != nil { + authorityURL = &wms130.AuthorityURL{ + Name: layer.Authority.Name, + OnlineResource: wms130.OnlineResource{ + Xlink: nil, + Type: nil, + Href: &layer.Authority.URL, + }, + } + identifier = &wms130.Identifier{ + Authority: layer.Authority.Name, + Value: layer.Authority.SpatialDatasetIdentifier, + } + } + + crsses, exBbox, bboxes, err := mapBBoxes(layer.BoundingBoxes, parentBBoxes) + if err != nil { + return nil, err + } + + l := wms130.Layer{ + Queryable: smoothoperatorutils.Pointer(1), + Opaque: nil, + Name: layer.Name, + Title: smoothoperatorutils.PointerVal(layer.Title, ""), + Abstract: smoothoperatorutils.Pointer(smoothoperatorutils.PointerVal(layer.Abstract, "")), + KeywordList: &wms130.Keywords{Keyword: layer.Keywords}, + CRS: crsses, + EXGeographicBoundingBox: exBbox, + BoundingBox: bboxes, + Dimension: nil, + Attribution: nil, + AuthorityURL: authorityURL, + Identifier: identifier, + DataURL: nil, + FeatureListURL: nil, + Style: getLayerStyles(layer, canonicalURL, parentStyleNames), + Layer: []*wms130.Layer{}, + } + + if layer.MinScaleDenominator != nil { + float, err := strconv.ParseFloat(*layer.MinScaleDenominator, 64) + if err != nil { + return nil, err + } + l.MinScaleDenominator = &float + + } + + if layer.MaxScaleDenominator != nil { + float, err := strconv.ParseFloat(*layer.MaxScaleDenominator, 64) + if err != nil { + return nil, err + } + l.MaxScaleDenominator = &float + } + + if layer.DatasetMetadataURL != nil { + l.MetadataURL = append(l.MetadataURL, &wms130.MetadataURL{ + Type: smoothoperatorutils.Pointer("TC211"), + Format: smoothoperatorutils.Pointer("text/plain"), + OnlineResource: wms130.OnlineResource{ + Xlink: smoothoperatorutils.Pointer(XLinkURL), + Type: smoothoperatorutils.Pointer("simple"), + Href: smoothoperatorutils.Pointer("https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=" + layer.DatasetMetadataURL.CSW.MetadataIdentifier), + }, + }) + } + + layerStyleNames := []string{} + for _, s := range l.Style { + layerStyleNames = append(layerStyleNames, s.Name) + } + + // Map sublayers + for _, sublayer := range layer.Layers { + if sublayer.Visible { + mapped, err := mapLayer(sublayer, canonicalURL, authorityURL, identifier, append(parentStyleNames, layerStyleNames...), bboxes) + if err != nil { + return nil, err + } + l.Layer = append(l.Layer, mapped) + } + } + + return &l, nil +} + +func mapBBoxes(layerBBoxes []pdoknlv3.WMSBoundingBox, parentBBoxes []*wms130.LayerBoundingBox) ([]wms130.CRS, *wms130.EXGeographicBoundingBox, []*wms130.LayerBoundingBox, error) { + bboxMap := make(map[string]*wms130.LayerBoundingBox) + crsstrings := []string{} + for _, bbox := range parentBBoxes { + crsstrings = append(crsstrings, bbox.CRS) + bboxMap[bbox.CRS] = bbox + } + for _, bbox := range layerBBoxes { + minX, err := strconv.ParseFloat(bbox.BBox.MinX, 64) + if err != nil { + return nil, nil, nil, err + } + minY, err := strconv.ParseFloat(bbox.BBox.MinY, 64) + if err != nil { + return nil, nil, nil, err + } + maxX, err := strconv.ParseFloat(bbox.BBox.MaxX, 64) + if err != nil { + return nil, nil, nil, err + } + maxY, err := strconv.ParseFloat(bbox.BBox.MaxY, 64) + if err != nil { + return nil, nil, nil, err + } + if !slices.Contains(crsstrings, bbox.CRS) { + crsstrings = append(crsstrings, bbox.CRS) + } + bboxMap[bbox.CRS] = &wms130.LayerBoundingBox{ + CRS: bbox.CRS, + Minx: minX, + Miny: minY, + Maxx: maxX, + Maxy: maxY, + } + } + + var exBbox *wms130.EXGeographicBoundingBox + bboxes := []*wms130.LayerBoundingBox{} + crsses := []wms130.CRS{} + + for _, crs := range crsstrings { + crsSplit := strings.Split(crs, ":") + code, err := strconv.Atoi(crsSplit[1]) + if err != nil { + return nil, nil, nil, err + } + crsses = append(crsses, wms130.CRS{ + Namespace: crsSplit[0], + Code: code, + }) + + bbox := bboxMap[crs] + bboxes = append(bboxes, bbox) + + if crs == "CRS:84" { + exBbox = &wms130.EXGeographicBoundingBox{ + WestBoundLongitude: bbox.Minx, + EastBoundLongitude: bbox.Maxx, + SouthBoundLatitude: bbox.Miny, + NorthBoundLatitude: bbox.Maxy, + } + } + + } + return crsses, exBbox, bboxes, nil +} + +func getLayerStyles(layer pdoknlv3.Layer, canonicalURL string, parentStyleNames []string) (styles []*wms130.Style) { + for _, style := range layer.Styles { + if slices.Contains(parentStyleNames, style.Name) { + continue + } + + newStyle := wms130.Style{ + Name: style.Name, + Title: smoothoperatorutils.PointerVal(style.Title, ""), + Abstract: style.Abstract, + LegendURL: &wms130.LegendURL{ + Width: 78, + Height: 20, + Format: "image/png", + OnlineResource: wms130.OnlineResource{ + Xlink: smoothoperatorutils.Pointer(XLinkURL), + Type: smoothoperatorutils.Pointer("simple"), + Href: smoothoperatorutils.Pointer(canonicalURL + "/legend/" + *layer.Name + "/" + style.Name + ".png"), + }, + }, + StyleSheetURL: nil, + } + styles = append(styles, &newStyle) + } + return +} diff --git a/internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml b/internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml new file mode 100644 index 0000000..043d61f --- /dev/null +++ b/internal/controller/capabilitiesgenerator/test_data/wfs_input.yaml @@ -0,0 +1,78 @@ +global: + prefix: prefix + namespace: http://prefix.geonovum.nl + onlineResourceUrl: http://localhost + path: /datasetOwner/dataset/theme/wfs/v1_0 + additionalSchemaLocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd +services: + wfs200: + filename: /var/www/config/capabilities_wfs_200.xml + definition: + serviceIdentification: + title: some Service title + abstract: 'some "Service" abstract' + keywords: + keyword: + - service-keyword-1 + - service-keyword-2 + - infoFeatureAccessService + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + serviceProvider: + providerName: PDOK + capabilities: + operationsMetadata: + extendedCapabilities: + extendedCapabilities: + metadataUrl: + url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=metameta-meta-meta-meta-metametameta + mediaType: application/vnd.ogc.csw.GetRecordByIdResponse_xml + supportedLanguages: + defaultLanguage: + language: dut + responseLanguage: + language: dut + spatialDataSetIdentifier: + code: datadata-data-data-data-datadatadata + featureTypeList: + featureType: + - name: prefix:featuretype-1-name + title: featuretype-1-title + abstract: 'feature "1" abstract' + keywords: + - keyword: + - featuretype-1-keyword-1 + - featuretype-1-keyword-2 + defaultCrs: urn:ogc:def:crs:EPSG::28992 + otherCrs: + - urn:ogc:def:crs:EPSG::28992 + - urn:ogc:def:crs:EPSG::25831 + - urn:ogc:def:crs:EPSG::25832 + - urn:ogc:def:crs:EPSG::3034 + - urn:ogc:def:crs:EPSG::3035 + - urn:ogc:def:crs:EPSG::3857 + - urn:ogc:def:crs:EPSG::4258 + - urn:ogc:def:crs:EPSG::4326 + wgs84BoundingBox: + lowerCorner: "-180 -90" + upperCorner: "180 90" + metadataUrl: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + - name: prefix:featuretype-2-name + title: featuretype-2-title + abstract: 'feature "2" abstract' + keywords: + - keyword: + - featuretype-2-keyword-1 + - featuretype-2-keyword-2 + defaultCrs: urn:ogc:def:crs:EPSG::28992 + otherCrs: + - urn:ogc:def:crs:EPSG::28992 + - urn:ogc:def:crs:EPSG::25831 + - urn:ogc:def:crs:EPSG::25832 + - urn:ogc:def:crs:EPSG::3034 + - urn:ogc:def:crs:EPSG::3035 + - urn:ogc:def:crs:EPSG::3857 + - urn:ogc:def:crs:EPSG::4258 + - urn:ogc:def:crs:EPSG::4326 + metadataUrl: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata diff --git a/internal/controller/capabilitiesgenerator/test_data/wms_input.yaml b/internal/controller/capabilitiesgenerator/test_data/wms_input.yaml new file mode 100644 index 0000000..2b33553 --- /dev/null +++ b/internal/controller/capabilitiesgenerator/test_data/wms_input.yaml @@ -0,0 +1,407 @@ +global: + prefix: nwbwegen + namespace: http://nwbwegen.geonovum.nl + onlineResourceUrl: http://localhost + path: /rws/nwbwegen/wms/v1_0 + additionalSchemaLocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd +services: + wms130: + filename: /var/www/config/capabilities_wms_130.xml + definition: + wmsCapabilities: + space: "" + local: "" + service: + name: WMS + title: NWB - Wegen WMS + abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien van een straatnaam of nummer. + keywordList: + keyword: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + - Hectometerpunten + - HVD + - Mobiliteit + - infoMapAccessService + onlineResource: + xlink: null + type: null + href: https://www.pdok.nl + contactInformation: + contactPersonPrimary: + contactPerson: KlantContactCenter PDOK + contactOrganization: PDOK + contactPosition: pointOfContact + contactAddress: + addressType: Work + address: "" + city: Apeldoorn + stateOrProvince: "" + postalCode: "" + country: The Netherlands + contactVoiceTelephone: null + contactFacsimileTelephone: null + contactElectronicMailAddress: BeheerPDOK@kadaster.nl + accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + optionalConstraints: + maxWidth: 4000 + maxHeight: 4000 + capability: + wmsCapabilities: + request: + getCapabilities: + format: + - text/xml + dcpType: + http: + get: + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: null + href: http://localhost/rws/nwbwegen/wms/v1_0? + post: null + getMap: + format: + - image/png + - image/jpeg + - image/png; mode=8bit + - image/vnd.jpeg-png + - image/vnd.jpeg-png8 + dcpType: + http: + get: + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: null + href: http://localhost/rws/nwbwegen/wms/v1_0? + post: + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: null + href: http://localhost/rws/nwbwegen/wms/v1_0 + getFeatureInfo: + format: + - application/json + - application/json; subtype=geojson + - application/vnd.ogc.gml + - text/html + - text/plain + - text/xml + - text/xml; subtype=gml/3.1.1 + dcpType: + http: + get: + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: null + href: http://localhost/rws/nwbwegen/wms/v1_0? + post: + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: null + href: http://localhost/rws/nwbwegen/wms/v1_0 + exception: + format: + - XML + - INIMAGE + - BLANK + extendedCapabilities: + metadataUrl: + url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8 + mediaType: application/vnd.ogc.csw.GetRecordByIdResponse_xml + supportedLanguages: + defaultLanguage: + language: dut + supportedLanguage: + - language: dut + responseLanguage: + language: dut + layer: + - queryable: 1 + title: NWB - Wegen WMS + abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien van een straatnaam of nummer. + keywordList: + keyword: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + - Hectometerpunten + - HVD + - Mobiliteit + crs: + - EPSG:28992 + - EPSG:25831 + - EPSG:25832 + - EPSG:3034 + - EPSG:3035 + - EPSG:3857 + - EPSG:4258 + - EPSG:4326 + - CRS:84 + exGeographicBoundingBox: # without this Qgis cannot use the wms + westBoundLongitude: 2.52713 + eastBoundLongitude: 7.37403 + southBoundLatitude: 50.2129 + northBoundLatitude: 55.7212 + boundingBox: # without this Qgis cannot use the wms + - crs: EPSG:28992 + minx: -59188.44333693248 + miny: 304984.64144318487 + maxx: 308126.88473339565 + maxy: 858328.516489961 + - crs: EPSG:25831 + minx: -470271 + miny: 5.56231e+06 + maxx: 795163 + maxy: 6.18197e+06 + - crs: EPSG:25832 + minx: 62461.6 + miny: 5.56555e+06 + maxx: 397827 + maxy: 6.19042e+06 + - crs: EPSG:3034 + minx: 2.61336e+06 + miny: 3.509e+06 + maxx: 3.22007e+06 + maxy: 3.84003e+06 + - crs: EPSG:3035 + minx: 3.01676e+06 + miny: 3.81264e+06 + maxx: 3.64485e+06 + maxy: 4.15586e+06 + - crs: EPSG:3857 + minx: 281318 + miny: 6.48322e+06 + maxx: 820873 + maxy: 7.50311e+06 + - crs: EPSG:4258 + minx: 50.2129 + miny: 2.52713 + maxx: 55.7212 + maxy: 7.37403 + - crs: EPSG:4326 + minx: 50.2129 + miny: 2.52713 + maxx: 55.7212 + maxy: 7.37403 + - crs: CRS:84 + minx: 2.52713 + miny: 50.2129 + maxx: 7.37403 + maxy: 55.7212 + layer: + - queryable: 1 + name: wegvakken + title: Wegvakken + abstract: Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, routenummer, wegbeheerder, huisnummers, enz. weer. + keywordList: + keyword: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + crs: + - EPSG:28992 + - EPSG:25831 + - EPSG:25832 + - EPSG:3034 + - EPSG:3035 + - EPSG:3857 + - EPSG:4258 + - EPSG:4326 + - CRS:84 + exGeographicBoundingBox: # without this Qgis cannot use the wms + westBoundLongitude: 2.52713 + eastBoundLongitude: 7.37403 + southBoundLatitude: 50.2129 + northBoundLatitude: 55.7212 + boundingBox: # without this Qgis cannot use the wms + - crs: EPSG:28992 + minx: -59188.44333693248 + miny: 304984.64144318487 + maxx: 308126.88473339565 + maxy: 858328.516489961 + - crs: EPSG:25831 + minx: -470271 + miny: 5.56231e+06 + maxx: 795163 + maxy: 6.18197e+06 + - crs: EPSG:25832 + minx: 62461.6 + miny: 5.56555e+06 + maxx: 397827 + maxy: 6.19042e+06 + - crs: EPSG:3034 + minx: 2.61336e+06 + miny: 3.509e+06 + maxx: 3.22007e+06 + maxy: 3.84003e+06 + - crs: EPSG:3035 + minx: 3.01676e+06 + miny: 3.81264e+06 + maxx: 3.64485e+06 + maxy: 4.15586e+06 + - crs: EPSG:3857 + minx: 281318 + miny: 6.48322e+06 + maxx: 820873 + maxy: 7.50311e+06 + - crs: EPSG:4258 + minx: 50.2129 + miny: 2.52713 + maxx: 55.7212 + maxy: 7.37403 + - crs: EPSG:4326 + minx: 50.2129 + miny: 2.52713 + maxx: 55.7212 + maxy: 7.37403 + - crs: CRS:84 + minx: 2.52713 + miny: 50.2129 + maxx: 7.37403 + maxy: 55.7212 + authorityUrl: + name: rws + onlineResource: + href: https://www.rijkswaterstaat.nl + type: null + xlink: null + identifier: + authority: rws + value: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + metadataUrl: + - type: TC211 + format: text/plain + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: simple + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=a9b7026e-0a81-4813-93bd-ba49e6f28502 + style: + - name: wegvakken + title: NWB - Wegvakken + legendUrl: + width: 78 + height: 20 + format: image/png + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: simple + href: http://localhost/rws/nwbwegen/wms/v1_0/legend/wegvakken/wegvakken.png + minScaleDenominator: 1 + maxScaleDenominator: 50000 + - queryable: 1 + name: hectopunten + title: Hectopunten + abstract: Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, zijde en hectoletter weer. + keywordList: + keyword: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Hectometerpunten + crs: + - EPSG:28992 + - EPSG:25831 + - EPSG:25832 + - EPSG:3034 + - EPSG:3035 + - EPSG:3857 + - EPSG:4258 + - EPSG:4326 + - CRS:84 + exGeographicBoundingBox: # without this Qgis cannot use the wms + westBoundLongitude: 2.52713 + eastBoundLongitude: 7.37403 + southBoundLatitude: 50.2129 + northBoundLatitude: 55.7212 + boundingBox: # without this Qgis cannot use the wms + - crs: EPSG:28992 + minx: -59188.44333693248 + miny: 304984.64144318487 + maxx: 308126.88473339565 + maxy: 858328.516489961 + - crs: EPSG:25831 + minx: -470271 + miny: 5.56231e+06 + maxx: 795163 + maxy: 6.18197e+06 + - crs: EPSG:25832 + minx: 62461.6 + miny: 5.56555e+06 + maxx: 397827 + maxy: 6.19042e+06 + - crs: EPSG:3034 + minx: 2.61336e+06 + miny: 3.509e+06 + maxx: 3.22007e+06 + maxy: 3.84003e+06 + - crs: EPSG:3035 + minx: 3.01676e+06 + miny: 3.81264e+06 + maxx: 3.64485e+06 + maxy: 4.15586e+06 + - crs: EPSG:3857 + minx: 281318 + miny: 6.48322e+06 + maxx: 820873 + maxy: 7.50311e+06 + - crs: EPSG:4258 + minx: 50.2129 + miny: 2.52713 + maxx: 55.7212 + maxy: 7.37403 + - crs: EPSG:4326 + minx: 50.2129 + miny: 2.52713 + maxx: 55.7212 + maxy: 7.37403 + - crs: CRS:84 + minx: 2.52713 + miny: 50.2129 + maxx: 7.37403 + maxy: 55.7212 + authorityUrl: + name: rws + onlineResource: + href: https://www.rijkswaterstaat.nl + type: null + xlink: null + identifier: + authority: rws + value: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + metadataUrl: + - type: TC211 + format: text/plain + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: simple + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=a9b7026e-0a81-4813-93bd-ba49e6f28502 + style: + - name: hectopunten + title: NWB - Hectopunten + legendUrl: + width: 78 + height: 20 + format: image/png + onlineResource: + xlink: http://www.w3.org/1999/xlink + type: simple + href: http://localhost/rws/nwbwegen/wms/v1_0/legend/hectopunten/hectopunten.png + minScaleDenominator: 1 + maxScaleDenominator: 50000 diff --git a/internal/controller/configmaps.go b/internal/controller/configmaps.go new file mode 100644 index 0000000..baea3fd --- /dev/null +++ b/internal/controller/configmaps.go @@ -0,0 +1,147 @@ +package controller + +import ( + "fmt" + "strings" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/blobdownload" + "github.com/pdok/mapserver-operator/internal/controller/capabilitiesgenerator" + "github.com/pdok/mapserver-operator/internal/controller/mapfilegenerator" + "github.com/pdok/mapserver-operator/internal/controller/static" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + downloadScriptName = "gpkg_download.sh" + mapfileGeneratorInput = "input.json" + capabilitiesGeneratorInput = "input.yaml" +) + +func mutateConfigMapCapabilitiesGenerator[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap, ownerInfo *smoothoperatorv1.OwnerInfo) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { + return err + } + + if len(configMap.Data) == 0 { + input, err := capabilitiesgenerator.GetInput(obj, ownerInfo) + if err != nil { + return err + } + configMap.Data = map[string]string{capabilitiesGeneratorInput: input} + } + configMap.Immutable = smoothoperatorutils.Pointer(true) + + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { + return err + } + if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { + return err + } + return smoothoperatorutils.AddHashSuffix(configMap) +} + +func mutateConfigMapMapfileGenerator[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap, ownerInfo *smoothoperatorv1.OwnerInfo) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { + return err + } + + if len(configMap.Data) == 0 { + mapfileGeneratorConfig, err := mapfilegenerator.GetConfig(obj, ownerInfo) + if err != nil { + return err + } + configMap.Data = map[string]string{mapfileGeneratorInput: mapfileGeneratorConfig} + } + configMap.Immutable = smoothoperatorutils.Pointer(true) + + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { + return err + } + if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { + return err + } + return smoothoperatorutils.AddHashSuffix(configMap) +} + +func mutateConfigMapBlobDownload[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { + return err + } + + if len(configMap.Data) == 0 { + downloadScript := blobdownload.GetScript() + configMap.Data = map[string]string{downloadScriptName: downloadScript} + } + configMap.Immutable = smoothoperatorutils.Pointer(true) + + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { + return err + } + if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { + return err + } + return smoothoperatorutils.AddHashSuffix(configMap) +} + +func getBareConfigMap[O pdoknlv3.WMSWFS](obj O, name string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: getSuffixedName(obj, name), + Namespace: obj.GetNamespace(), + }, + } +} + +func mutateConfigMap[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, configMap *corev1.ConfigMap) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, configMap, labels); err != nil { + return err + } + + configMap.Immutable = smoothoperatorutils.Pointer(true) + configMap.Data = map[string]string{} + + updateConfigMapWithStaticFiles(configMap, obj) + + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, configMap, configMap); err != nil { + return err + } + if err := ctrl.SetControllerReference(obj, configMap, getReconcilerScheme(r)); err != nil { + return err + } + return smoothoperatorutils.AddHashSuffix(configMap) +} + +func updateConfigMapWithStaticFiles[O pdoknlv3.WMSWFS](configMap *corev1.ConfigMap, obj O) { + staticFileName, contents := static.GetStaticFiles() + for _, name := range staticFileName { + content := contents[name] + if name == "include.conf" { + ingressRouteUrls := obj.IngressRouteURLs(true) + rewriteRules := make([]string, 0) + for _, ingressRouteURL := range ingressRouteUrls { + rewriteRules = append(rewriteRules, fmt.Sprintf(" \"%s/legend(.*)\" => \"/legend$1\"", ingressRouteURL.URL.Path)) + rewriteRules = append(rewriteRules, fmt.Sprintf(" \"%s(.*)\" => \"/mapserver$1\"", ingressRouteURL.URL.Path)) + } + + content = []byte(strings.ReplaceAll(string(content), "{{ rewrite_rules }}", strings.Join(rewriteRules, ",\n"))) + } + configMap.Data[name] = string(content) + } +} diff --git a/internal/controller/configmaps_test.go b/internal/controller/configmaps_test.go new file mode 100644 index 0000000..143a467 --- /dev/null +++ b/internal/controller/configmaps_test.go @@ -0,0 +1,31 @@ +package controller + +import ( + "os" + "testing" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/constants" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +func TestMapserverConfigMaps(t *testing.T) { + wfsBytes, err := os.ReadFile("test_data/wfs/complete/input/wfs.yaml") + assert.NoError(t, err) + o := &pdoknlv3.WFS{} + err = yaml.Unmarshal(wfsBytes, o) + assert.NoError(t, err) + generatedConfigMap := getBareConfigMap(o, constants.MapserverName) + generatedConfigMap.Data = make(map[string]string) + updateConfigMapWithStaticFiles(generatedConfigMap, o) + + expectedConfigMap := v1.ConfigMap{} + expectedBytes, err := os.ReadFile("test_data/wfs/complete/expected/configmap-mapserver.yaml") + assert.NoError(t, err) + err = yaml.Unmarshal(expectedBytes, &expectedConfigMap) + assert.NoError(t, err) + + assert.Equal(t, expectedConfigMap.Data, generatedConfigMap.Data) +} diff --git a/internal/controller/constants/constants.go b/internal/controller/constants/constants.go new file mode 100644 index 0000000..8e9eb6c --- /dev/null +++ b/internal/controller/constants/constants.go @@ -0,0 +1,29 @@ +package constants + +const ( + MapserverName = "mapserver" + OgcWebserviceProxyName = "ogc-webservice-proxy" + MapfileGeneratorName = "mapfile-generator" + CapabilitiesGeneratorName = "capabilities-generator" + BlobDownloadName = "blob-download" + InitScriptsName = "init-scripts" + LegendGeneratorName = "legend-generator" + LegendFixerName = "legend-fixer" + FeatureinfoGeneratorName = "featureinfo-generator" + + BaseVolumeName = "base" + DataVolumeName = "data" + + configSuffix = "-config" + ConfigMapMapfileGeneratorVolumeName = MapfileGeneratorName + configSuffix + ConfigMapStylingFilesVolumeName = "styling-files" + ConfigMapCapabilitiesGeneratorVolumeName = CapabilitiesGeneratorName + configSuffix + ConfigMapOgcWebserviceProxyVolumeName = OgcWebserviceProxyName + configSuffix + ConfigMapLegendGeneratorVolumeName = LegendGeneratorName + configSuffix + ConfigMapFeatureinfoGeneratorVolumeName = FeatureinfoGeneratorName + configSuffix + ConfigMapCustomMapfileVolumeName = "mapfile" + + HTMLTemplatesPath = "/srv/data/config/templates" + MapserverPortNr int32 = 80 + ApachePortNr int32 = 9117 +) diff --git a/internal/controller/deployment.go b/internal/controller/deployment.go new file mode 100644 index 0000000..97be2c6 --- /dev/null +++ b/internal/controller/deployment.go @@ -0,0 +1,296 @@ +package controller + +import ( + "strconv" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/blobdownload" + "github.com/pdok/mapserver-operator/internal/controller/capabilitiesgenerator" + "github.com/pdok/mapserver-operator/internal/controller/constants" + "github.com/pdok/mapserver-operator/internal/controller/featureinfogenerator" + "github.com/pdok/mapserver-operator/internal/controller/legendgenerator" + "github.com/pdok/mapserver-operator/internal/controller/mapfilegenerator" + "github.com/pdok/mapserver-operator/internal/controller/mapperutils" + "github.com/pdok/mapserver-operator/internal/controller/mapserver" + "github.com/pdok/mapserver-operator/internal/controller/ogcwebserviceproxy" + "github.com/pdok/mapserver-operator/internal/controller/types" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" +) + +var storageClassName string + +func SetStorageClassName(name string) { + storageClassName = name +} + +func getBareDeployment[O pdoknlv3.WMSWFS](obj O) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: getSuffixedName(obj, constants.MapserverName), + // name might become too long. not handling here. will just fail on apply. + Namespace: obj.GetNamespace(), + }, + } +} + +func mutateDeployment[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, deployment *appsv1.Deployment, configMapNames types.HashedConfigMapNames) error { + reconcilerClient := getReconcilerClient(r) + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, deployment, labels); err != nil { + return err + } + + deployment.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels} + + deployment.Spec.RevisionHistoryLimit = smoothoperatorutils.Pointer(int32(1)) + deployment.Spec.Strategy = appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxUnavailable: &intstr.IntOrString{IntVal: 1}, + MaxSurge: &intstr.IntOrString{IntVal: 1}, + }, + } + + initContainers, err := getInitContainerForDeployment(r, obj) + if err != nil { + return err + } + setTerminationMessage(initContainers) + + images := getReconcilerImages(r) + containers, err := getContainers(obj, images) + if err != nil { + return err + } + setTerminationMessage(containers) + + volumes := getVolumes(obj, configMapNames) + + podTemplateSpec := corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: getPodAnnotations(deployment), + Labels: labels, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyAlways, + DNSPolicy: corev1.DNSClusterFirst, + TerminationGracePeriodSeconds: smoothoperatorutils.Pointer(int64(60)), + InitContainers: initContainers, + Containers: containers, + Volumes: volumes, + }, + } + + podPatch := obj.PodSpecPatch() + patchedSpec, err := smoothoperatorutils.StrategicMergePatch(&podTemplateSpec.Spec, &podPatch) + if err != nil { + return err + } + podTemplateSpec.Spec = *patchedSpec + + if use, _ := mapperutils.UseEphemeralVolume(obj); !use { + ephStorage := podTemplateSpec.Spec.Containers[0].Resources.Limits[corev1.ResourceEphemeralStorage] + threshold := resource.MustParse("200M") + + if ephStorage.Value() < threshold.Value() { + podTemplateSpec.Spec.Containers[0].Resources.Limits[corev1.ResourceEphemeralStorage] = threshold + } + } else { + delete(podTemplateSpec.Spec.Containers[0].Resources.Limits, corev1.ResourceEphemeralStorage) + delete(podTemplateSpec.Spec.Containers[0].Resources.Requests, corev1.ResourceEphemeralStorage) + } + + deployment.Spec.Template = podTemplateSpec + + if err = smoothoperatorutils.EnsureSetGVK(reconcilerClient, deployment, deployment); err != nil { + return err + } + return ctrl.SetControllerReference(obj, deployment, getReconcilerScheme(r)) +} + +func getPodAnnotations(deployment *appsv1.Deployment) map[string]string { + annotations := smoothoperatorutils.CloneOrEmptyMap(deployment.Spec.Template.GetAnnotations()) + annotations["cluster-autoscaler.kubernetes.io/safe-to-evict"] = "true" + annotations["kubectl.kubernetes.io/default-container"] = constants.MapserverName + annotations["match-regex.version-checker.io/mapserver"] = `^\d\.\d\.\d.*$` + annotations["prometheus.io/scrape"] = "true" + annotations["prometheus.io/port"] = strconv.Itoa(int(constants.ApachePortNr)) + annotations["priority.version-checker.io/mapserver"] = "4" + annotations["priority.version-checker.io/ogc-webservice-proxy"] = "4" + return annotations +} + +func getInitContainerForDeployment[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O) ([]corev1.Container, error) { + + images := getReconcilerImages(r) + blobDownloadInitContainer, err := blobdownload.GetBlobDownloadInitContainer(obj, *images) + if err != nil { + return nil, err + } + capabilitiesGeneratorInitContainer, err := capabilitiesgenerator.GetCapabilitiesGeneratorInitContainer(obj, *images) + if err != nil { + return nil, err + } + + initContainers := []corev1.Container{ + *blobDownloadInitContainer, + *capabilitiesGeneratorInitContainer, + } + + if obj.Mapfile() == nil { + mapfileGeneratorInitContainer, err := mapfilegenerator.GetMapfileGeneratorInitContainer(obj, *images) + if err != nil { + return nil, err + } + initContainers = append(initContainers, *mapfileGeneratorInitContainer) + } + + if wms, ok := any(obj).(*pdoknlv3.WMS); ok { + featureInfoInitContainer, err := featureinfogenerator.GetFeatureinfoGeneratorInitContainer(*images) + if err != nil { + return nil, err + } + initContainers = append(initContainers, *featureInfoInitContainer) + + legendGeneratorInitContainer, err := legendgenerator.GetLegendGeneratorInitContainer(wms, *images) + if err != nil { + return nil, err + } + initContainers = append(initContainers, *legendGeneratorInitContainer) + + if wms.Options().RewriteGroupToDataLayers { + legendFixerInitContainer := legendgenerator.GetLegendFixerInitContainer(*images) + initContainers = append(initContainers, *legendFixerInitContainer) + } + + } + return initContainers, nil +} + +func getContainers[O pdoknlv3.WMSWFS](obj O, images *types.Images) ([]corev1.Container, error) { + mapserverContainer, err := mapserver.GetMapserverContainer(obj, *images) + if err != nil { + return nil, err + } + containers := []corev1.Container{ + *mapserverContainer, + getApacheContainer(*images), + } + if wms, ok := any(obj).(*pdoknlv3.WMS); ok { + ogcProxy, err := ogcwebserviceproxy.GetOgcWebserviceProxyContainer(wms, *images) + if err != nil { + return nil, err + } + containers = append(containers, *ogcProxy) + } + return containers, nil +} + +func getApacheContainer(images types.Images) corev1.Container { + return corev1.Container{ + Name: "apache-exporter", + Image: images.ApacheExporterImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{{ContainerPort: constants.ApachePortNr, Protocol: corev1.ProtocolTCP}}, + Args: []string{"--scrape_uri=http://localhost/server-status?auto"}, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{corev1.ResourceMemory: resource.MustParse("48M")}, + Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("0.02")}, + }, + } +} + +func setTerminationMessage(c []corev1.Container) { + for i := range c { + c[i].TerminationMessagePolicy = "File" + c[i].TerminationMessagePath = "/dev/termination-log" + } +} + +func getVolumes[O pdoknlv3.WMSWFS](obj O, configMapNames types.HashedConfigMapNames) []corev1.Volume { + baseVolume := corev1.Volume{Name: constants.BaseVolumeName} + if use, size := mapperutils.UseEphemeralVolume(obj); use { + baseVolume.Ephemeral = &corev1.EphemeralVolumeSource{ + VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{ + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.VolumeResourceRequirements{Requests: corev1.ResourceList{ + corev1.ResourceStorage: *size, + }}, + }, + }, + } + if storageClassName != "" { + baseVolume.Ephemeral.VolumeClaimTemplate.Spec.StorageClassName = &storageClassName + } + } else { + baseVolume.EmptyDir = &corev1.EmptyDirVolumeSource{} + } + + volumes := []corev1.Volume{ + baseVolume, + {Name: constants.DataVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, + getConfigMapVolume(constants.MapserverName, configMapNames.Mapserver), + } + + if mapfile := obj.Mapfile(); mapfile != nil { + volumes = append(volumes, getConfigMapVolume(constants.ConfigMapCustomMapfileVolumeName, mapfile.ConfigMapKeyRef.Name)) + } + + if obj.Type() == pdoknlv3.ServiceTypeWMS && obj.Options().UseWebserviceProxy() { + volumes = append(volumes, getConfigMapVolume(constants.ConfigMapOgcWebserviceProxyVolumeName, configMapNames.OgcWebserviceProxy)) + } + + if obj.Options().PrefetchData { + vol := getConfigMapVolume(constants.InitScriptsName, configMapNames.InitScripts) + vol.ConfigMap.DefaultMode = smoothoperatorutils.Pointer(int32(0777)) + volumes = append(volumes, vol) + } + + volumes = append(volumes, getConfigMapVolume(constants.ConfigMapCapabilitiesGeneratorVolumeName, configMapNames.CapabilitiesGenerator)) + + if obj.Mapfile() == nil { + volumes = append(volumes, getConfigMapVolume(constants.ConfigMapMapfileGeneratorVolumeName, configMapNames.MapfileGenerator)) + } + + if obj.Type() == pdoknlv3.ServiceTypeWMS { + if obj.Mapfile() == nil { + wms, _ := any(obj).(*pdoknlv3.WMS) + volumeProjections := []corev1.VolumeProjection{} + for _, cm := range wms.Spec.Service.StylingAssets.ConfigMapRefs { + volumeProjections = append(volumeProjections, corev1.VolumeProjection{ + ConfigMap: &corev1.ConfigMapProjection{LocalObjectReference: corev1.LocalObjectReference{Name: cm.Name}}, + }) + } + + volumes = append(volumes, corev1.Volume{ + Name: constants.ConfigMapStylingFilesVolumeName, + VolumeSource: corev1.VolumeSource{Projected: &corev1.ProjectedVolumeSource{Sources: volumeProjections}}, + }) + } + + volumes = append( + volumes, + getConfigMapVolume(constants.ConfigMapFeatureinfoGeneratorVolumeName, configMapNames.FeatureInfoGenerator), + getConfigMapVolume(constants.ConfigMapLegendGeneratorVolumeName, configMapNames.LegendGenerator), + ) + } + + return volumes +} + +func getConfigMapVolume(name, configMap string) corev1.Volume { + return corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ + DefaultMode: smoothoperatorutils.Pointer(int32(0644)), + LocalObjectReference: corev1.LocalObjectReference{Name: configMap}}, + }, + } +} diff --git a/internal/controller/featureinfogenerator/featureinfo_generator.go b/internal/controller/featureinfogenerator/featureinfo_generator.go new file mode 100644 index 0000000..d1fe3b1 --- /dev/null +++ b/internal/controller/featureinfogenerator/featureinfo_generator.go @@ -0,0 +1,49 @@ +package featureinfogenerator + +import ( + "encoding/json" + "fmt" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/types" + "github.com/pdok/mapserver-operator/internal/controller/utils" + corev1 "k8s.io/api/core/v1" +) + +func GetFeatureinfoGeneratorInitContainer(images types.Images) (*corev1.Container, error) { + initContainer := corev1.Container{ + Name: constants.FeatureinfoGeneratorName, + Image: images.FeatureinfoGeneratorImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{constants.FeatureinfoGeneratorName}, + Args: []string{ + "--input-path", + "/input/input.json", + "--dest-folder", + constants.HTMLTemplatesPath, + "--file-name", + "feature-info", + }, + VolumeMounts: []corev1.VolumeMount{ + utils.GetBaseVolumeMount(), + utils.GetConfigVolumeMount(constants.ConfigMapFeatureinfoGeneratorVolumeName), + }, + } + + return &initContainer, nil +} + +func GetInput(wms *pdoknlv3.WMS) (string, error) { + input, err := MapWMSToFeatureinfoGeneratorInput(wms) + if err != nil { + return "", err + } + jsonInput, err := json.MarshalIndent(input, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal the featureinfo generator input to json: %w", err) + } + + return string(jsonInput), nil +} diff --git a/internal/controller/featureinfogenerator/featureinfo_generator_test.go b/internal/controller/featureinfogenerator/featureinfo_generator_test.go new file mode 100644 index 0000000..5fb0045 --- /dev/null +++ b/internal/controller/featureinfogenerator/featureinfo_generator_test.go @@ -0,0 +1,142 @@ +package featureinfogenerator + +import ( + "testing" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +const ( + featureInfoGeneratorInput = `{ + "automaticCasing": true, + "version": 2, + "layers": [ + { + "name": "gpkg-layer-name", + "groupName": "group-layer-name", + "properties": [ + { + "name": "fuuid" + }, + { + "name": "column-1", + "alias": "ALIAS_column-1" + }, + { + "name": "column-2" + } + ] + }, + { + "name": "postgis-layer-name", + "groupName": "group-layer-name", + "properties": [ + { + "name": "fuuid" + }, + { + "name": "column-1" + }, + { + "name": "column-2" + } + ] + }, + { + "name": "tif-layer-name", + "groupName": "group-layer-name", + "properties": [ + { + "name": "value_list" + }, + { + "name": "class" + } + ] + } + ] +}` +) + +func TestGetInput(t *testing.T) { + type args struct { + wms *pdoknlv3.WMS + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "GetInput for featureinfoGenerator", + args: args{ + wms: &pdoknlv3.WMS{ + Spec: pdoknlv3.WMSSpec{ + Options: &pdoknlv3.Options{ + BaseOptions: pdoknlv3.BaseOptions{AutomaticCasing: true}, + }, + Service: pdoknlv3.WMSService{ + Layer: pdoknlv3.Layer{ + Name: smoothoperatorutils.Pointer("top-layer-name"), + Layers: []pdoknlv3.Layer{ + { + Name: smoothoperatorutils.Pointer("group-layer-name"), + Layers: []pdoknlv3.Layer{ + { + Name: smoothoperatorutils.Pointer("gpkg-layer-name"), + Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ + Gpkg: &pdoknlv3.Gpkg{ + Columns: []pdoknlv3.Column{ + {Name: "column-1", Alias: smoothoperatorutils.Pointer("ALIAS_column-1")}, + {Name: "column-2"}, + }, + }}, + }, + }, + { + Name: smoothoperatorutils.Pointer("postgis-layer-name"), + Data: &pdoknlv3.Data{BaseData: pdoknlv3.BaseData{ + Postgis: &pdoknlv3.Postgis{ + Columns: []pdoknlv3.Column{ + {Name: "column-1"}, + {Name: "column-2"}, + }, + }}, + }, + }, + { + Name: smoothoperatorutils.Pointer("tif-layer-name"), + Data: &pdoknlv3.Data{ + TIF: &pdoknlv3.TIF{ + GetFeatureInfoIncludesClass: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: featureInfoGeneratorInput, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetInput(tt.args.wms) + if (err != nil) != tt.wantErr { + t.Errorf("GetInput() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetInput() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/controller/featureinfogenerator/mapper.go b/internal/controller/featureinfogenerator/mapper.go new file mode 100644 index 0000000..188ddcd --- /dev/null +++ b/internal/controller/featureinfogenerator/mapper.go @@ -0,0 +1,69 @@ +package featureinfogenerator + +import ( + featureinfo "github.com/pdok/featureinfo-generator/pkg/types" + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +const ( + featureinfoGeneratorSchemaVersion = 2 +) + +func MapWMSToFeatureinfoGeneratorInput(wms *pdoknlv3.WMS) (*featureinfo.Scheme, error) { + + input := &featureinfo.Scheme{ + AutomaticCasing: wms.Options().AutomaticCasing, + Version: featureinfoGeneratorSchemaVersion, + Layers: []featureinfo.Layer{}, + } + + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if !layer.IsDataLayer { + continue + } + l := featureinfo.Layer{ + Name: *layer.Name, + Properties: getProperties(&layer.Layer), + } + + if layer.GroupName != nil && layer.GroupName != wms.Spec.Service.Layer.Name { + l.GroupName = smoothoperatorutils.PointerVal(layer.GroupName, "") + } + + input.Layers = append(input.Layers, l) + } + return input, nil +} + +func getProperties(layer *pdoknlv3.Layer) (properties []featureinfo.Property) { + switch { + case layer.Data.Gpkg != nil: + properties = getPropertiesForVector(layer.Data.Gpkg.Columns) + case layer.Data.Postgis != nil: + properties = getPropertiesForVector(layer.Data.Postgis.Columns) + case layer.Data.TIF != nil: + properties = getPropertiesForRaster(&layer.Data.TIF.GetFeatureInfoIncludesClass) + } + return +} + +func getPropertiesForVector(columns []pdoknlv3.Column) (properties []featureinfo.Property) { + properties = append(properties, featureinfo.Property{Name: "fuuid"}) + for _, column := range columns { + prop := featureinfo.Property{Name: column.Name} + if column.Alias != nil { + prop.Alias = *column.Alias + } + properties = append(properties, prop) + } + return +} + +func getPropertiesForRaster(includeClass *bool) (properties []featureinfo.Property) { + properties = append(properties, featureinfo.Property{Name: "value_list"}) + if includeClass != nil && *includeClass { + properties = append(properties, featureinfo.Property{Name: "class"}) + } + return +} diff --git a/internal/controller/horizontalpodautoscaler.go b/internal/controller/horizontalpodautoscaler.go new file mode 100644 index 0000000..d5cb2bb --- /dev/null +++ b/internal/controller/horizontalpodautoscaler.go @@ -0,0 +1,97 @@ +package controller + +import ( + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/constants" + "github.com/pdok/mapserver-operator/internal/controller/mapperutils" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +func mutateHorizontalPodAutoscaler[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, autoscaler *autoscalingv2.HorizontalPodAutoscaler) error { + reconcilerClient := getReconcilerClient(r) + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, autoscaler, labels); err != nil { + return err + } + + autoscaler.Spec.MaxReplicas = 30 + autoscaler.Spec.MinReplicas = smoothoperatorutils.Pointer(int32(2)) + autoscaler.Spec.ScaleTargetRef = autoscalingv2.CrossVersionObjectReference{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + Name: getSuffixedName(obj, constants.MapserverName), + } + + var averageCPU int32 = 90 + if cpu := mapperutils.GetContainerResourceRequest(obj, constants.MapserverName, corev1.ResourceCPU); cpu != nil { + averageCPU = 80 + } + autoscaler.Spec.Metrics = []autoscalingv2.MetricSpec{{ + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: corev1.ResourceCPU, + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: &averageCPU, + }, + }, + }} + + var behaviourStabilizationWindowSeconds int32 + if obj.Type() == pdoknlv3.ServiceTypeWFS { + behaviourStabilizationWindowSeconds = 300 + } + + autoscaler.Spec.Behavior = &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleUp: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &behaviourStabilizationWindowSeconds, + Policies: []autoscalingv2.HPAScalingPolicy{{ + Type: autoscalingv2.PodsScalingPolicy, + Value: 20, + PeriodSeconds: 60, + }}, + SelectPolicy: smoothoperatorutils.Pointer(autoscalingv2.MaxChangePolicySelect), + }, + ScaleDown: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: smoothoperatorutils.Pointer(int32(3600)), + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PercentScalingPolicy, + Value: 10, + PeriodSeconds: 600, + }, + { + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: 600, + }, + }, + SelectPolicy: smoothoperatorutils.Pointer(autoscalingv2.MaxChangePolicySelect), + }, + } + if obj.HorizontalPodAutoscalerPatch() != nil { + patchedSpec, err := smoothoperatorutils.StrategicMergePatch(&autoscaler.Spec, obj.HorizontalPodAutoscalerPatch()) + if err != nil { + return err + } + autoscaler.Spec = *patchedSpec + } + if err := smoothoperatorutils.EnsureSetGVK(getReconcilerClient(r), autoscaler, autoscaler); err != nil { + return err + } + return ctrl.SetControllerReference(obj, autoscaler, getReconcilerScheme(r)) +} + +func getBareHorizontalPodAutoScaler[O pdoknlv3.WMSWFS](obj O) *autoscalingv2.HorizontalPodAutoscaler { + return &autoscalingv2.HorizontalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: getSuffixedName(obj, constants.MapserverName), + Namespace: obj.GetNamespace(), + }, + } +} diff --git a/internal/controller/ingressroute.go b/internal/controller/ingressroute.go new file mode 100644 index 0000000..4863aa4 --- /dev/null +++ b/internal/controller/ingressroute.go @@ -0,0 +1,161 @@ +package controller + +import ( + "regexp" + "strings" + + smoothoperatormodel "github.com/pdok/smooth-operator/model" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + "github.com/pdok/mapserver-operator/internal/controller/utils" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" +) + +var setUptimeOperatorAnnotations = true + +func SetUptimeOperatorAnnotations(set bool) { + setUptimeOperatorAnnotations = set +} + +func getBareIngressRoute[O pdoknlv3.WMSWFS](obj O) *traefikiov1alpha1.IngressRoute { + return &traefikiov1alpha1.IngressRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: getSuffixedName(obj, constants.MapserverName), + Namespace: obj.GetNamespace(), + }, + } +} + +func mutateIngressRoute[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, ingressRoute *traefikiov1alpha1.IngressRoute) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, ingressRoute, labels); err != nil { + return err + } + + annotations := smoothoperatorutils.CloneOrEmptyMap(obj.GetAnnotations()) + if setUptimeOperatorAnnotations { + tags := []string{"public-stats", strings.ToLower(string(obj.Type()))} + + if obj.Inspire() != nil { + tags = append(tags, "inspire") + } + + queryString, _, err := obj.ReadinessQueryString() + if err != nil { + return err + } + + annotations["uptime.pdok.nl/id"] = utils.Sha1Hash(obj.TypedName()) + annotations["uptime.pdok.nl/name"] = getUptimeName(obj) + annotations["uptime.pdok.nl/url"] = obj.URL().String() + "?" + queryString + annotations["uptime.pdok.nl/tags"] = strings.Join(tags, ",") + } + ingressRoute.SetAnnotations(annotations) + + mapserverService := traefikiov1alpha1.Service{ + LoadBalancerSpec: traefikiov1alpha1.LoadBalancerSpec{ + Name: getBareService(obj).GetName(), + Kind: "Service", + Port: intstr.IntOrString{ + Type: intstr.Int, + IntVal: constants.MapserverPortNr, + }, + }, + } + + webServiceProxyService := traefikiov1alpha1.Service{ + LoadBalancerSpec: traefikiov1alpha1.LoadBalancerSpec{ + Name: getBareService(obj).GetName(), + Kind: "Service", + Port: intstr.IntOrString{ + Type: intstr.Int, + IntVal: int32(mapserverWebserviceProxyPortNr), + }, + }, + } + + middlewareRef := traefikiov1alpha1.MiddlewareRef{ + Name: getBareCorsHeadersMiddleware(obj).GetName(), + } + + makeRoute := func(match string, service traefikiov1alpha1.Service, middlewareRef traefikiov1alpha1.MiddlewareRef) traefikiov1alpha1.Route { + return traefikiov1alpha1.Route{ + Kind: "Rule", + Match: match, + Services: []traefikiov1alpha1.Service{service}, + Middlewares: []traefikiov1alpha1.MiddlewareRef{middlewareRef}, + } + } + + ingressRoute.Spec.Routes = []traefikiov1alpha1.Route{} + if obj.Type() == pdoknlv3.ServiceTypeWMS { + for _, ingressRouteURL := range obj.IngressRouteURLs(true) { + ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getLegendMatchRule(ingressRouteURL.URL), mapserverService, middlewareRef)) + + if obj.Options().UseWebserviceProxy() { + ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getMatchRule(ingressRouteURL.URL), webServiceProxyService, middlewareRef)) + } else { + ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getMatchRule(ingressRouteURL.URL), mapserverService, middlewareRef)) + } + } + } else { // WFS + for _, ingressRouteURL := range obj.IngressRouteURLs(true) { + ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, makeRoute(getMatchRule(ingressRouteURL.URL), mapserverService, middlewareRef)) + } + } + + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, ingressRoute, ingressRoute); err != nil { + return err + } + return ctrl.SetControllerReference(obj, ingressRoute, getReconcilerScheme(r)) +} + +// getUptimeName transforms the CR name into a uptime.pdok.nl/name value +// owner-dataset-v1-0 -> OWNER dataset v1_0 [INSPIRE] [WMS|WFS] +func getUptimeName[O pdoknlv3.WMSWFS](obj O) string { + // Extract the version from the CR name, owner-dataset-v1-0 -> owner-dataset + v1-0 + versionMatcher := regexp.MustCompile("^(.*)(?:-(v?[1-9](?:-[0-9])?))?$") + match := versionMatcher.FindStringSubmatch(obj.GetName()) + + nameParts := strings.Split(match[1], "-") + nameParts[0] = strings.ToUpper(nameParts[0]) + + // Add service version if found + if len(match) > 2 && len(match[2]) > 0 { + nameParts = append(nameParts, strings.ReplaceAll(match[2], "-", "_")) + } + + // Add inspire + if obj.Inspire() != nil { + nameParts = append(nameParts, "INSPIRE") + } + + return strings.Join(append(nameParts, string(obj.Type())), " ") +} + +func getMatchRule(url smoothoperatormodel.URL) string { + host := url.Hostname() + if strings.Contains(host, "localhost") { + return "Host(`localhost`) && Path(`" + url.Path + "`)" + } + + return "(Host(`localhost`) || Host(`" + host + "`)) && Path(`" + url.Path + "`)" +} + +func getLegendMatchRule(url smoothoperatormodel.URL) string { + host := url.Hostname() + if strings.Contains(host, "localhost") { + return "Host(`localhost`) && PathPrefix(`" + url.Path + "/legend`)" + } + + return "(Host(`localhost`) || Host(`" + host + "`)) && PathPrefix(`" + url.Path + "/legend`)" +} diff --git a/internal/controller/legendgenerator/legend-fixer.sh b/internal/controller/legendgenerator/legend-fixer.sh new file mode 100755 index 0000000..188bce4 --- /dev/null +++ b/internal/controller/legendgenerator/legend-fixer.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -eo pipefail +echo "creating legends for root and group layers by concatenating data layers" +input_filepath="/input/input" +remove_filepath="/input/remove" +config_filepath="/input/ogc-webservice-proxy-config.yaml" +legend_dir="/var/www/legend" +< "${input_filepath}" xargs -n 2 echo | while read -r layer style; do + export layer + # shellcheck disable=SC2016 # dollar is for yq + if ! < "${config_filepath}" yq -e 'env(layer) as $layer | .grouplayers | keys | contains([$layer])' &>/dev/null; then + continue + fi + export grouplayer="${layer}" + grouplayer_style_filepath="${legend_dir}/${grouplayer}/${style}.png" + # shellcheck disable=SC2016 # dollar is for yq + datalayers=$(< "${config_filepath}" yq 'env(grouplayer) as $foo | .grouplayers[$foo][]') + datalayer_style_filepaths=() + for datalayer in $datalayers; do + datalayer_style_filepath="${legend_dir}/${datalayer}/${style}.png" + if [[ -f "${datalayer_style_filepath}" ]]; then + datalayer_style_filepaths+=("${datalayer_style_filepath}") + fi + done + if [[ -n "${datalayer_style_filepaths[*]}" ]]; then + echo "concatenating ${grouplayer_style_filepath}" + gm convert -append "${datalayer_style_filepaths[@]}" "${grouplayer_style_filepath}" + else + echo "no data for ${grouplayer_style_filepath}" + fi +done +< "${remove_filepath}" xargs -n 2 echo | while read -r layer style; do + remove_legend_file="${legend_dir}/${layer}/${style}.png" + echo removing $remove_legend_file + rm $remove_legend_file +done +echo "done" \ No newline at end of file diff --git a/internal/controller/legendgenerator/legend_generator.go b/internal/controller/legendgenerator/legend_generator.go new file mode 100644 index 0000000..22935d8 --- /dev/null +++ b/internal/controller/legendgenerator/legend_generator.go @@ -0,0 +1,85 @@ +package legendgenerator + +import ( + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/constants" + "github.com/pdok/mapserver-operator/internal/controller/mapserver" + "github.com/pdok/mapserver-operator/internal/controller/types" + "github.com/pdok/mapserver-operator/internal/controller/utils" + corev1 "k8s.io/api/core/v1" +) + +func GetLegendGeneratorInitContainer(wms *pdoknlv3.WMS, images types.Images) (*corev1.Container, error) { + initContainer := corev1.Container{ + Name: constants.LegendGeneratorName, + Image: images.MapserverImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Env: []corev1.EnvVar{ + { + Name: "MAPSERVER_CONFIG_FILE", + Value: "/srv/mapserver/config/default_mapserver.conf", + }, + mapserver.GetMapfileEnvVar(wms), + }, + Command: []string{ + "bash", + "-c", + `set -eu; +exit_code=0; +cat /input/input | xargs -n 2 echo | while read layer style; do +echo Generating legend for layer: $layer, style: $style; +mkdir -p /var/www/legend/$layer; +mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; +magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); +if [[ $magic_bytes != 'PNG' ]]; then +echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; +exit_code=1; +fi; +done; +exit $exit_code; +`, + }, + VolumeMounts: []corev1.VolumeMount{ + utils.GetBaseVolumeMount(), + utils.GetDataVolumeMount(), + {Name: constants.MapserverName, MountPath: "/srv/mapserver/config/default_mapserver.conf", SubPath: "default_mapserver.conf"}, + }, + } + + if wms.Spec.Service.Mapfile != nil { + initContainer.VolumeMounts = append(initContainer.VolumeMounts, utils.GetMapfileVolumeMount()) + } + + // Adding config volumemount here to get the same order as in the old ansible operator + initContainer.VolumeMounts = append(initContainer.VolumeMounts, utils.GetConfigVolumeMount(constants.ConfigMapLegendGeneratorVolumeName)) + + return &initContainer, nil +} + +func GetLegendFixerInitContainer(images types.Images) *corev1.Container { + return &corev1.Container{ + Name: constants.LegendFixerName, + Image: images.MultitoolImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "/bin/bash", + "/input/legend-fixer.sh", + }, + VolumeMounts: []corev1.VolumeMount{ + utils.GetDataVolumeMount(), + utils.GetConfigVolumeMount(constants.ConfigMapLegendGeneratorVolumeName), + }, + } +} + +func GetConfigMapData(wms *pdoknlv3.WMS) map[string]string { + data := map[string]string{ + "default_mapserver.conf": defaultMapserverConf, + } + + addLayerInput(wms, data) + if wms.Options().RewriteGroupToDataLayers { + addLegendFixerConfig(wms, data) + } + return data +} diff --git a/internal/controller/legendgenerator/legend_generator_test.go b/internal/controller/legendgenerator/legend_generator_test.go new file mode 100644 index 0000000..0bd4d0c --- /dev/null +++ b/internal/controller/legendgenerator/legend_generator_test.go @@ -0,0 +1,42 @@ +package legendgenerator + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/pdok/mapserver-operator/api/v2beta1" + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" +) + +func test(t *testing.T, name string) { + input, err := os.ReadFile("test_data/input/" + name + ".yaml") + assert.NoError(t, err) + var v2wms v2beta1.WMS + err = yaml.Unmarshal(input, &v2wms) + assert.NoError(t, err) + var wms pdoknlv3.WMS + err = v2wms.ToV3(&wms) + assert.NoError(t, err) + + expected, err := os.ReadFile("test_data/expected/" + name + ".yaml") + assert.NoError(t, err) + + expectedMap := make(map[string]string) + err = yaml.Unmarshal(expected, &expectedMap) + assert.NoError(t, err) + + diff := cmp.Diff(expectedMap, GetConfigMapData(&wms)) + assert.Equal(t, diff, "", "diff in %s, -want +got: %s", name, diff) +} + +func TestGetConfigMapDataNoLegendFix(t *testing.T) { + test(t, "no-legend-fix") +} + +func TestGetConfigMapDataLegendFix(t *testing.T) { + test(t, "legend-fix") +} diff --git a/internal/controller/legendgenerator/mapper.go b/internal/controller/legendgenerator/mapper.go new file mode 100644 index 0000000..bf2a312 --- /dev/null +++ b/internal/controller/legendgenerator/mapper.go @@ -0,0 +1,140 @@ +package legendgenerator + +import ( + "fmt" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + + _ "embed" + "strings" + + "sigs.k8s.io/yaml" +) + +// TODO Reuse default_mapserver.conf from static_files? +const ( + defaultMapserverConf = `CONFIG + ENV + MS_MAP_NO_PATH "true" + END +END +` +) + +//go:embed legend-fixer.sh +var legendFixerScript string + +type LegendReference struct { + Layer string `yaml:"layer" json:"layer"` + Style string `yaml:"style" json:"style"` +} + +type OgcWebserviceProxyConfig struct { + GroupLayers map[string][]string `yaml:"grouplayers" json:"grouplayers"` +} + +func addLayerInput(wms *pdoknlv3.WMS, data map[string]string) { + legendReferences := make([]LegendReference, 0) + + processLayer(&wms.Spec.Service.Layer, &legendReferences) + + sb := strings.Builder{} + for _, reference := range legendReferences { + sb.WriteString(fmt.Sprintf("\"%s\" \"%s\"\n", reference.Layer, reference.Style)) + } + + data["input"] = sb.String() + + // TODO - current config is hard to read and process down the line + // We should alter the legend generation script so it can handle yaml input and pass the data as follows + // referencesYaml, err := yaml.Marshal(legendReferences) + // if err == nil { + // data["input2"] = string(referencesYaml) + // } +} + +func processLayer(layer *pdoknlv3.Layer, legendReferences *[]LegendReference) { + if !layer.Visible { + return + } + for _, style := range layer.Styles { + if style.Legend == nil { + *legendReferences = append(*legendReferences, LegendReference{ + Layer: *layer.Name, + Style: style.Name, + }) + } + } + + if layer.Layers != nil { + for _, innerLayer := range layer.Layers { + processLayer(&innerLayer, legendReferences) + } + } +} + +func addLegendFixerConfig(wms *pdoknlv3.WMS, data map[string]string) { + data["legend-fixer.sh"] = legendFixerScript + + topLayer := wms.Spec.Service.Layer + + legendReferences := make([]LegendReference, 0) + topLevelStyleNames := make(map[string]bool) + + for _, style := range topLayer.Styles { + topLevelStyleNames[style.Name] = true + } + + if topLayer.Layers != nil { + // These layers are called 'middle layers' in the old operator + for _, layer := range wms.Spec.Service.Layer.Layers { + for _, style := range layer.Styles { + if topLevelStyleNames[style.Name] && style.Legend == nil { + legendReferences = append(legendReferences, LegendReference{ + Layer: *layer.Name, + Style: style.Name, + }) + } + } + } + } + + sb := strings.Builder{} + for _, reference := range legendReferences { + sb.WriteString(fmt.Sprintf("\"%s\" \"%s\"\n", reference.Layer, reference.Style)) + } + + data["remove"] = sb.String() + + groupLayers := make(map[string][]string) + + if topLayer.IsGroupLayer() && topLayer.Name != nil { + layerName := topLayer.Name + targetArray := make([]string, 0) + getAllNestedNonGroupLayerNames(&topLayer, &targetArray) + groupLayers[*layerName] = targetArray + + for _, subLayer := range topLayer.Layers { + if subLayer.IsGroupLayer() { + layerName = subLayer.Name + targetArray = make([]string, 0) + getAllNestedNonGroupLayerNames(&subLayer, &targetArray) + groupLayers[*layerName] = targetArray + } + } + } + + ogcWebServiceProxyConfig := OgcWebserviceProxyConfig{GroupLayers: groupLayers} + proxyConfigData, _ := yaml.Marshal(ogcWebServiceProxyConfig) + data["ogc-webservice-proxy-config.yaml"] = string(proxyConfigData) +} + +func getAllNestedNonGroupLayerNames(layer *pdoknlv3.Layer, target *[]string) { + for _, subLayer := range layer.Layers { + if subLayer.IsGroupLayer() { + getAllNestedNonGroupLayerNames(&subLayer, target) + } else { + *target = append(*target, *subLayer.Name) + } + } +} diff --git a/internal/controller/legendgenerator/test_data/expected/legend-fix.yaml b/internal/controller/legendgenerator/test_data/expected/legend-fix.yaml new file mode 100644 index 0000000..a2149e5 --- /dev/null +++ b/internal/controller/legendgenerator/test_data/expected/legend-fix.yaml @@ -0,0 +1,121 @@ +default_mapserver.conf: | + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + END +input: | + "Kadastralekaart" "standaard" + "Kadastralekaart" "kwaliteit" + "Kadastralekaart" "print" + "Bebouwing" "standaard:bebouwing" + "Bebouwing" "kwaliteit:bebouwing" + "Bebouwing" "print:bebouwing" + "Bebouwingvlak" "standaard" + "Bebouwingvlak" "kwaliteit" + "Bebouwingvlak" "print" + "Bebouwingvlak" "standaard:bebouwing" + "Bebouwingvlak" "kwaliteit:bebouwing" + "Bebouwingvlak" "print:bebouwing" + "Nummeraanduidingreeks" "standaard" + "Nummeraanduidingreeks" "kwaliteit" + "Nummeraanduidingreeks" "print" + "Nummeraanduidingreeks" "standaard:bebouwing" + "Nummeraanduidingreeks" "kwaliteit:bebouwing" + "Nummeraanduidingreeks" "print:bebouwing" + "OpenbareRuimteNaam" "standaard" + "OpenbareRuimteNaam" "kwaliteit" + "OpenbareRuimteNaam" "print" + "OpenbareRuimteNaam" "standaard:openbareruimtenaam" + "OpenbareRuimteNaam" "kwaliteit:openbareruimtenaam" + "OpenbareRuimteNaam" "print:openbareruimtenaam" + "Perceel" "standaard:perceel" + "Perceel" "kwaliteit:perceel" + "Perceel" "print:perceel" + "Perceelvlak" "standaard" + "Perceelvlak" "kwaliteit" + "Perceelvlak" "print" + "Perceelvlak" "standaard:perceel" + "Perceelvlak" "kwaliteit:perceel" + "Perceelvlak" "print:perceel" + "Label" "standaard" + "Label" "standaard:perceel" + "Label" "kwaliteit" + "Label" "kwaliteit:perceel" + "Label" "print" + "Label" "print:perceel" + "Bijpijling" "standaard" + "Bijpijling" "kwaliteit" + "Bijpijling" "print" + "Bijpijling" "standaard:perceel" + "Bijpijling" "kwaliteit:perceel" + "Bijpijling" "print:perceel" + "KadastraleGrens" "standaard" + "KadastraleGrens" "kwaliteit" + "KadastraleGrens" "print" + "KadastraleGrens" "standaard:kadastralegrens" + "KadastraleGrens" "kwaliteit:kadastralegrens" + "KadastraleGrens" "print:kadastralegrens" +legend-fixer.sh: |- + #!/usr/bin/env bash + set -eo pipefail + echo "creating legends for root and group layers by concatenating data layers" + input_filepath="/input/input" + remove_filepath="/input/remove" + config_filepath="/input/ogc-webservice-proxy-config.yaml" + legend_dir="/var/www/legend" + < "${input_filepath}" xargs -n 2 echo | while read -r layer style; do + export layer + # shellcheck disable=SC2016 # dollar is for yq + if ! < "${config_filepath}" yq -e 'env(layer) as $layer | .grouplayers | keys | contains([$layer])' &>/dev/null; then + continue + fi + export grouplayer="${layer}" + grouplayer_style_filepath="${legend_dir}/${grouplayer}/${style}.png" + # shellcheck disable=SC2016 # dollar is for yq + datalayers=$(< "${config_filepath}" yq 'env(grouplayer) as $foo | .grouplayers[$foo][]') + datalayer_style_filepaths=() + for datalayer in $datalayers; do + datalayer_style_filepath="${legend_dir}/${datalayer}/${style}.png" + if [[ -f "${datalayer_style_filepath}" ]]; then + datalayer_style_filepaths+=("${datalayer_style_filepath}") + fi + done + if [[ -n "${datalayer_style_filepaths[*]}" ]]; then + echo "concatenating ${grouplayer_style_filepath}" + gm convert -append "${datalayer_style_filepaths[@]}" "${grouplayer_style_filepath}" + else + echo "no data for ${grouplayer_style_filepath}" + fi + done + < "${remove_filepath}" xargs -n 2 echo | while read -r layer style; do + remove_legend_file="${legend_dir}/${layer}/${style}.png" + echo removing $remove_legend_file + rm $remove_legend_file + done + echo "done" +ogc-webservice-proxy-config.yaml: | + grouplayers: + Bebouwing: + - Bebouwingvlak + - Nummeraanduidingreeks + Kadastralekaart: + - Bebouwingvlak + - Nummeraanduidingreeks + - OpenbareRuimteNaam + - Perceelvlak + - Label + - Bijpijling + - KadastraleGrens + Perceel: + - Perceelvlak + - Label + - Bijpijling +remove: | + "OpenbareRuimteNaam" "standaard" + "OpenbareRuimteNaam" "kwaliteit" + "OpenbareRuimteNaam" "print" + "KadastraleGrens" "standaard" + "KadastraleGrens" "kwaliteit" + "KadastraleGrens" "print" + \ No newline at end of file diff --git a/internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml b/internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml new file mode 100644 index 0000000..298b271 --- /dev/null +++ b/internal/controller/legendgenerator/test_data/expected/no-legend-fix.yaml @@ -0,0 +1,14 @@ +default_mapserver.conf: | + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + END +input: | + "wegvakken" "wegvakken" + "hectopunten" "hectopunten" +#input2: | +# - layer: wegvakken +# style: wegvakken +# - layer: hectopunten +# style: hectopunten diff --git a/internal/controller/legendgenerator/test_data/input/legend-fix.yaml b/internal/controller/legendgenerator/test_data/input/legend-fix.yaml new file mode 100644 index 0000000..ba779ca --- /dev/null +++ b/internal/controller/legendgenerator/test_data/input/legend-fix.yaml @@ -0,0 +1,527 @@ +apiVersion: pdok.nl/v2beta1 +kind: WMS +metadata: + name: kadaster-kadastralekaart + labels: + dataset-owner: kadaster + dataset: kadastralekaart + service-version: v5_0 + service-type: wms +spec: + general: + datasetOwner: kadaster + dataset: kadastralekaart + serviceVersion: v5_0 + kubernetes: + healthCheck: + querystring: language=dut&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=193882.0336615453998,470528.1693874415942,193922.4213813782844,470564.250484353397&CRS=EPSG:28992&WIDTH=769&HEIGHT=687&LAYERS=OpenbareRuimteNaam,Bebouwing,Perceel,KadastraleGrens&FORMAT=image/png&DPI=96&MAP_RESOLUTION=96&FORMAT_OPTIONS=dpi:96&TRANSPARENT=TRUE + mimetype: image/png + resources: + limits: + memory: "100M" + ephemeralStorage: "200M" + requests: + cpu: "500" + memory: "100M" + ephemeralStorage: "100M" + options: + automaticCasing: true + disableWebserviceProxy: false + includeIngress: true + validateRequests: true + rewriteGroupToDataLayers: true + service: + inspire: false + title: Kadastrale Kaart (WMS) + abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen informatie kan vastleggen en presenteren. + keywords: + - Kadaster + - Kadastrale percelen + - Kadastrale grens + - Kadastrale kaart + - Bebouwing + - Nummeraanduidingreeks + - Openbare ruimte naam + - Perceel + - Grens + - Kwaliteit + - Kwaliteitslabels + - HVD + - Geospatiale data + metadataIdentifier: 97cf6a64-9cfc-4ce6-9741-2db44fd27fca + authority: + name: kadaster + url: https://www.kadaster.nl + dataEPSG: EPSG:28992 + resolution: 91 + defResolution: 91 + extent: "-25000 250000 280000 860000" + maxSize: 10000 + stylingAssets: + configMapRefs: + - name: ${INCLUDES} + blobKeys: + - ${BLOBS_RESOURCES_BUCKET}/fonts/liberation-sans.ttf + - ${BLOBS_RESOURCES_BUCKET}/fonts/liberation-sans-italic.ttf + layers: + - name: Kadastralekaart + title: KadastraleKaartv5 + abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen informatie kan vastleggen en presenteren. + maxScale: 6001 + keywords: + - Kadaster + - Kadastrale percelen + - Kadastrale grens + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + - name: Bebouwing + visible: true + group: Kadastralekaart + title: Bebouwing + abstract: De laag Bebouwing is een selectie op panden van de BGT. + keywords: + - Bebouwing + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 6001 + styles: + - name: standaard:bebouwing + title: Standaardvisualisatie Bebouwing + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + - name: kwaliteit:bebouwing + title: Kwaliteitsvisualisatie Bebouwing + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + - name: print:bebouwing + title: Printvisualisatie Bebouwing + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + - name: Bebouwingvlak + visible: true + group: Bebouwing + title: Bebouwingvlak + abstract: De laag Bebouwing is een selectie op panden van de BGT. + keywords: + - Bebouwing + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 6001 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: bebouwing.style + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: bebouwing_kwaliteit.style + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: bebouwing_print.style + - name: standaard:bebouwing + title: Standaardvisualisatie Bebouwing + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: bebouwing.group.style + - name: kwaliteit:bebouwing + title: Kwaliteitsvisualisatie Bebouwing + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: bebouwing_kwaliteit.group.style + - name: print:bebouwing + title: Printvisualisatie Bebouwing + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: bebouwing_print.group.style + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/pand.gpkg + table: pand + geometryType: Polygon + columns: + - object_begin_tijd + - lv_publicatiedatum + - relatieve_hoogteligging + - in_onderzoek + - tijdstip_registratie + - identificatie_namespace + - identificatie_lokaal_id + - bronhouder + - bgt_status + - plus_status + - identificatie_bag_pnd + aliases: + lv_publicatiedatum: LV-publicatiedatum + identificatie_lokaal_id: identificatieLokaalID + identificatie_bag_pnd: identificatieBAGPND + bgt_status: bgt-status + plus_status: plus-status + - name: Nummeraanduidingreeks + visible: true + group: Bebouwing + title: Nummeraanduidingreeks + abstract: De laag Bebouwing is een selectie op panden van de BGT. + keywords: + - Nummeraanduidingreeks + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 2001 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaarvisualisatie van de nummeraanduidingreeks. + visualization: nummeraanduidingreeks.style + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: nummeraanduidingreeks_kwaliteit.style + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: nummeraanduidingreeks_print.style + - name: standaard:bebouwing + title: Standaardvisualisatie Bebouwing + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: nummeraanduidingreeks.group.style + - name: kwaliteit:bebouwing + title: Kwaliteitsvisualisatie Bebouwing + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: nummeraanduidingreeks_kwaliteit.group.style + - name: print:bebouwing + title: Printvisualisatie Bebouwing + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: nummeraanduidingreeks_print.group.style + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/pand_nummeraanduiding.gpkg + table: pand_nummeraanduiding + geometryType: Point + columns: + - bebouwing_id + - hoek + - tekst + - bag_vbo_laagste_huisnummer + - bag_vbo_hoogste_huisnummer + - hoek + aliases: + bebouwing_id: bebouwingID + bag_vbo_laagste_huisnummer: identificatie_BAGVBOLaagsteHuisnummer + bag_vbo_hoogste_huisnummer: identificatie_BAGVBOHoogsteHuisnummer + - name: OpenbareRuimteNaam + visible: true + group: Kadastralekaart + title: OpenbareRuimteNaam + abstract: De laag Openbareruimtenaam is een selectie op de openbare ruimte labels van de BGT met een bgt-status "bestaand" die een classificatie (openbareruimtetype) Weg en Water hebben. + keywords: + - Openbare ruimte naam + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 2001 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: openbareruimtenaam.style + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: openbareruimtenaam_kwaliteit.style + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: openbareruimtenaam_print.style + - name: standaard:openbareruimtenaam + title: Standaardvisualisatie OpenbareRuimteNaam + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: openbareruimtenaam.group.style + - name: kwaliteit:openbareruimtenaam + title: Kwaliteitsvisualisatie OpenbareRuimteNaam + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: openbareruimtenaam_kwaliteit.group.style + - name: print:openbareruimtenaam + title: Printvisualisatie OpenbareRuimteNaam + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: openbareruimtenaam_print.group.style + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/openbareruimtelabel.gpkg + table: openbareruimtelabel + geometryType: Point + columns: + - object_begin_tijd + - lv_publicatiedatum + - relatieve_hoogteligging + - in_onderzoek + - tijdstip_registratie + - identificatie_namespace + - identificatie_lokaal_id + - bronhouder + - bgt_status + - plus_status + - identificatie_bag_opr + - tekst + - hoek + - openbare_ruimte_type + aliases: + lv_publicatiedatum: LV-publicatiedatum + identificatie_lokaal_id: identificatieLokaalID + identificatie_bag_opr: identificatieBAGOPR + bgt_status: bgt-status + plus_status: plus-status + - name: Perceel + visible: true + group: Kadastralekaart + title: Perceel + abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. + keywords: + - Perceel + - Kadastrale percelen + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 6001 + styles: + - name: standaard:perceel + title: Standaardvisualisatie Perceel + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + - name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + - name: print:perceel + title: Printvisualisatie Perceel + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + - name: Perceelvlak + visible: true + group: Perceel + title: Perceelvlak + abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. + keywords: + - Kadastrale percelen + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 6001 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: perceelvlak.style + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: perceelvlak_kwaliteit.style + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: perceelvlak_print.style + - name: standaard:perceel + title: Standaardvisualisatie Perceel + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: perceelvlak.group.style + - name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: perceelvlak_kwaliteit.group.style + - name: print:perceel + title: Printvisualisatie Perceel + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: perceelvlak_print.group.style + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/perceel.gpkg + table: perceel + geometryType: Polygon + columns: + - identificatie_namespace + - identificatie_lokaal_id + - begin_geldigheid + - tijdstip_registratie + - volgnummer + - status_historie_code + - status_historie_waarde + - kadastrale_gemeente_code + - kadastrale_gemeente_waarde + - sectie + - akr_kadastrale_gemeente_code_code + - akr_kadastrale_gemeente_code_waarde + - kadastrale_grootte_waarde + - soort_grootte_code + - soort_grootte_waarde + - perceelnummer + - perceelnummer_rotatie + - perceelnummer_verschuiving_delta_x + - perceelnummer_verschuiving_delta_y + - perceelnummer_plaatscoordinaat_x + - perceelnummer_plaatscoordinaat_y + aliases: + identificatie_lokaal_id: identificatieLokaalID + akr_kadastrale_gemeente_code_code: AKRKadastraleGemeenteCodeCode + akr_kadastrale_gemeente_code_waarde: AKRKadastraleGemeenteCodeWaarde + - name: Label + visible: true + group: Perceel + title: Label + abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. + keywords: + - Kadastrale percelen + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 6001 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaarvisualisatie van het label. + visualization: label.style + - name: standaard:perceel + title: Standaardvisualisatie Perceel + abstract: Standaarvisualisatie van het label. + visualization: label.group.style + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: label_kwaliteit.style + - name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: label_kwaliteit.group.style + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: label_print.style + - name: print:perceel + title: Printvisualisatie Perceel + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: label_print.group.style + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/perceel_label.gpkg + table: perceel_label + geometryType: Point + columns: + - perceel_id + - perceelnummer + - rotatie + - verschuiving_delta_x + - verschuiving_delta_y + aliases: + perceel_id: perceelID + - name: Bijpijling + visible: true + group: Perceel + title: Bijpijling + abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is. + keywords: + - Kadastrale percelen + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 6001 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: bijpijling.style + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: bijpijling_kwaliteit.style + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: bijpijling_print.style + - name: standaard:perceel + title: Standaardvisualisatie Perceel + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: bijpijling.group.style + - name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: bijpijling_kwaliteit.group.style + - name: print:perceel + title: Printvisualisatie Perceel + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: bijpijling_print.group.style + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/perceel_bijpijling.gpkg + table: perceel_bijpijling + geometryType: LineString + columns: + - perceel_id + aliases: + perceel_id: perceelID + - name: KadastraleGrens + visible: true + group: Kadastralekaart + title: KadastraleGrens + abstract: Een Kadastrale Grens is de weergave van een grens op de kadastrale kaart die door de dienst van het Kadaster tussen percelen (voorlopig) vastgesteld wordt, op basis van inlichtingen van belanghebbenden en met gebruikmaking van de aan de kadastrale kaart ten grondslag liggende bescheiden die in elk geval de landmeetkundige gegevens bevatten van hetgeen op die kaart wordt weergegeven. + keywords: + - Grens + - Kadastrale grenzen + datasetMetadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + sourceMetadataIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + minScale: 50 + maxScale: 6001 + styles: + - name: standaard + title: Standaardvisualisatie + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: kadastralegrens.style + - name: kwaliteit + title: Kwaliteitsvisualisatie + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: kadastralegrens_kwaliteit.style + - name: print + title: Printvisualisatie + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: kadastralegrens_print.style + - name: standaard:kadastralegrens + title: Standaardvisualisatie KadastraleGrens + abstract: Standaardvisualisatie met grenzen op basis van type (definitief, voorlopig of administratief). + visualization: kadastralegrens.group.style + - name: kwaliteit:kadastralegrens + title: Kwaliteitsvisualisatie KadastraleGrens + abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse (B, C, D of E). + visualization: kadastralegrens_kwaliteit.group.style + - name: print:kadastralegrens + title: Printvisualisatie KadastraleGrens + abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + visualization: kadastralegrens_print.group.style + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/kadaster/kadastralekaart_brk/${GPKG_VERSION}/kadastrale_grens.gpkg + table: kadastrale_grens + geometryType: LineString + columns: + - begin_geldigheid + - tijdstip_registratie + - volgnummer + - status_historie_code + - status_historie_waarde + - identificatie_namespace + - identificatie_lokaal_id + - type_grens_code + - type_grens_waarde + - classificatie_kwaliteit_code + - classificatie_kwaliteit_waarde + - perceel_links_identificatie_namespace + - perceel_links_identificatie_lokaal_id + - perceel_rechts_identificatie_namespace + - perceel_rechts_identificatie_lokaal_id + aliases: + identificatie_lokaal_id: identificatieLokaalID + perceel_links_identificatie_lokaal_id: perceelLinksIdentificatieLokaalID + perceel_rechts_identificatie_lokaal_id: perceelRechtsIdentificatieLokaalID + classificatie_kwaliteit_code: ClassificatieKwaliteitCode + classificatie_kwaliteit_waarde: ClassificatieKwaliteitWaarde diff --git a/internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml b/internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml new file mode 100644 index 0000000..7cacc96 --- /dev/null +++ b/internal/controller/legendgenerator/test_data/input/no-legend-fix.yaml @@ -0,0 +1,193 @@ +apiVersion: pdok.nl/v2beta1 +kind: WMS +metadata: + name: rws-nwbwegen-v1-0 + labels: + dataset-owner: rws + dataset: nwbwegen + service-version: v1_0 + service-type: wms + annotations: + lifecycle-phase: prod + service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb +spec: + general: + datasetOwner: rws + dataset: nwbwegen + serviceVersion: v1_0 + kubernetes: + healthCheck: + boundingbox: 135134.89,457152.55,135416.03,457187.82 + resources: + limits: + ephemeralStorage: 1535Mi + memory: 4G + requests: + cpu: 2000m + ephemeralStorage: 1535Mi + memory: 4G + options: + automaticCasing: true + disableWebserviceProxy: false + includeIngress: true + validateRequests: true + service: + title: NWB - Wegen WMS + abstract: + Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. + Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen + Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. + Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, + provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien + van een straatnaam of nummer. + authority: + name: rws + url: https://www.rijkswaterstaat.nl + dataEPSG: EPSG:28992 + extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961 + inspire: true + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + - Hectometerpunten + - HVD + - Mobiliteit + stylingAssets: + configMapRefs: + - name: includes + keys: + - nwb_wegen_hectopunten.symbol + - hectopunten.style + - wegvakken.style + blobKeys: + - resources/fonts/liberation-sans.ttf + layers: + - abstract: + Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) + en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, + routenummer, wegbeheerder, huisnummers, enz. weer. + data: + gpkg: + columns: + - objectid + - wvk_id + - wvk_begdat + - jte_id_beg + - jte_id_end + - wegbehsrt + - wegnummer + - wegdeelltr + - hecto_lttr + - bst_code + - rpe_code + - admrichtng + - rijrichtng + - stt_naam + - stt_bron + - wpsnaam + - gme_id + - gme_naam + - hnrstrlnks + - hnrstrrhts + - e_hnr_lnks + - e_hnr_rhts + - l_hnr_lnks + - l_hnr_rhts + - begafstand + - endafstand + - beginkm + - eindkm + - pos_tv_wol + - wegbehcode + - wegbehnaam + - distrcode + - distrnaam + - dienstcode + - dienstnaam + - wegtype + - wgtype_oms + - routeltr + - routenr + - routeltr2 + - routenr2 + - routeltr3 + - routenr3 + - routeltr4 + - routenr4 + - wegnr_aw + - wegnr_hmp + - geobron_id + - geobron_nm + - bronjaar + - openlr + - bag_orl + - frc + - fow + - alt_naam + - alt_nr + - rel_hoogte + - st_lengthshape + geometryType: MultiLineString + blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg + table: wegvakken + datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + maxScale: 50000.0 + minScale: 1.0 + name: wegvakken + sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + styles: + - name: wegvakken + title: NWB - Wegvakken + visualization: wegvakken.style + title: Wegvakken + visible: true + - abstract: + Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) + en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, + zijde en hectoletter weer. + data: + gpkg: + columns: + - objectid + - hectomtrng + - afstand + - wvk_id + - wvk_begdat + - zijde + - hecto_lttr + geometryType: MultiPoint + blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg + table: hectopunten + datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Hectometerpunten + maxScale: 50000.0 + minScale: 1.0 + name: hectopunten + sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + styles: + - name: hectopunten + title: NWB - Hectopunten + visualization: hectopunten.style + title: Hectopunten + visible: true + metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8 diff --git a/internal/controller/mapfilegenerator/mapfile_generator.go b/internal/controller/mapfilegenerator/mapfile_generator.go new file mode 100644 index 0000000..694a62e --- /dev/null +++ b/internal/controller/mapfilegenerator/mapfile_generator.go @@ -0,0 +1,84 @@ +package mapfilegenerator + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + "github.com/pdok/mapserver-operator/internal/controller/types" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/utils" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + corev1 "k8s.io/api/core/v1" +) + +func GetMapfileGeneratorInitContainer[O pdoknlv3.WMSWFS](obj O, images types.Images) (*corev1.Container, error) { + initContainer := corev1.Container{ + Name: constants.MapfileGeneratorName, + Image: images.MapfileGeneratorImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{"generate-mapfile"}, + Args: []string{ + "--not-include", + strings.ToLower(string(obj.Type())), + "/input/input.json", + "/srv/data/config/mapfile", + }, + VolumeMounts: []corev1.VolumeMount{ + utils.GetBaseVolumeMount(), + utils.GetConfigVolumeMount(constants.ConfigMapMapfileGeneratorVolumeName), + }, + } + + if obj.Type() == pdoknlv3.ServiceTypeWMS { + stylingFilesVolMount := corev1.VolumeMount{Name: constants.ConfigMapStylingFilesVolumeName, MountPath: "/styling", ReadOnly: true} + initContainer.VolumeMounts = append(initContainer.VolumeMounts, stylingFilesVolMount) + } + + return &initContainer, nil +} + +func GetConfig[W pdoknlv3.WMSWFS](webservice W, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { + switch any(webservice).(type) { + case *pdoknlv3.WFS: + if WFS, ok := any(webservice).(*pdoknlv3.WFS); ok { + return createConfigForWFS(WFS, ownerInfo) + } + case *pdoknlv3.WMS: + if WMS, ok := any(webservice).(*pdoknlv3.WMS); ok { + return createConfigForWMS(WMS, ownerInfo) + } + default: + return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) + } + return "", fmt.Errorf("unexpected input, webservice should be of type WFS or WMS, webservice: %v", webservice) +} + +func createConfigForWFS(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { + input, err := MapWFSToMapfileGeneratorInput(wfs, ownerInfo) + if err != nil { + return "", err + } + + jsonConfig, err := json.MarshalIndent(input, "", " ") + if err != nil { + return "", err + } + return string(jsonConfig), nil +} + +func createConfigForWMS(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (config string, err error) { + input, err := MapWMSToMapfileGeneratorInput(wms, ownerInfo) + if err != nil { + return "", err + } + + jsonConfig, err := json.MarshalIndent(input, "", " ") + if err != nil { + return "", err + } + return string(jsonConfig), nil +} diff --git a/internal/controller/mapfilegenerator/mapfile_generator_test.go b/internal/controller/mapfilegenerator/mapfile_generator_test.go new file mode 100644 index 0000000..3d6726e --- /dev/null +++ b/internal/controller/mapfilegenerator/mapfile_generator_test.go @@ -0,0 +1,110 @@ +package mapfilegenerator + +import ( + "encoding/json" + "os" + "testing" + + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/google/go-cmp/cmp" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +func TestGetConfigForWFS(t *testing.T) { + pdoknlv3.SetHost("https://service.pdok.nl") + ownerInfo := &smoothoperatorv1.OwnerInfo{ + Spec: smoothoperatorv1.OwnerInfoSpec{ + NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), + }, + } + + input, err := os.ReadFile("test_data/input/wfs.yaml") + assert.NoError(t, err) + inputWfs := pdoknlv3.WFS{} + err = yaml.Unmarshal(input, &inputWfs) + assert.NoError(t, err) + warnings := []string{} + allErrs := field.ErrorList{} + pdoknlv3.ValidateWFS(&inputWfs, &warnings, &allErrs) + + inputStruct, err := MapWFSToMapfileGeneratorInput(&inputWfs, ownerInfo) + assert.NoError(t, err) + expected, err := readExpectedWFS("wfs.json") + assert.NoError(t, err) + + diff := cmp.Diff(expected, inputStruct) + assert.Equal(t, diff, "", "%s", diff) +} + +func TestGetConfigForWMSWithNoGroupLayers(t *testing.T) { + testWMS(t, "wms_groupless") +} + +func TestGetConfigForWMSWithGroupLayers(t *testing.T) { + testWMS(t, "wms_group") +} + +func TestGetConfigForWMSWithGroupLayersAndTopGroupLayer(t *testing.T) { + testWMS(t, "wms_group_and_toplayer") +} + +func TestGetConfigForTifWMS(t *testing.T) { + testWMS(t, "wms_tif") +} + +func TestGetConfigForPostgisWMS(t *testing.T) { + testWMS(t, "wms_postgis") +} + +func testWMS(t *testing.T, filenameWithoutExt string) { + pdoknlv3.SetHost("https://service.pdok.nl") + ownerInfo := &smoothoperatorv1.OwnerInfo{ + Spec: smoothoperatorv1.OwnerInfoSpec{ + NamespaceTemplate: smoothoperatorutils.Pointer("http://{{prefix}}.geonovum.nl"), + }, + } + + input, err := os.ReadFile("test_data/input/" + filenameWithoutExt + ".yaml") + assert.NoError(t, err) + var wms pdoknlv3.WMS + err = yaml.Unmarshal(input, &wms) + assert.NoError(t, err) + inputStruct, err := MapWMSToMapfileGeneratorInput(&wms, ownerInfo) + assert.NoError(t, err) + expected, err := readExpectedWMS(filenameWithoutExt + ".json") + assert.NoError(t, err) + + diff := cmp.Diff(expected, inputStruct) + assert.Equal(t, diff == "", true, "%s", diff) +} + +func readExpectedWMS(filename string) (WMSInput, error) { + bytes, err := os.ReadFile("test_data/expected/" + filename) + if err != nil { + return WMSInput{}, err + } + + expected := WMSInput{} + err = json.Unmarshal(bytes, &expected) + + return expected, err +} + +func readExpectedWFS(filename string) (WFSInput, error) { + bytes, err := os.ReadFile("test_data/expected/" + filename) + if err != nil { + return WFSInput{}, err + } + + expected := WFSInput{} + err = json.Unmarshal(bytes, &expected) + + return expected, err +} diff --git a/internal/controller/mapfilegenerator/mapper.go b/internal/controller/mapfilegenerator/mapper.go new file mode 100644 index 0000000..ce3121e --- /dev/null +++ b/internal/controller/mapfilegenerator/mapper.go @@ -0,0 +1,325 @@ +package mapfilegenerator + +import ( + "slices" + "strconv" + "strings" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/mapperutils" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +const ( + defaultMaxFeatures = 1000 + tifPath = "/srv/data/tif" + geopackagePath = "/srv/data/gpkg" + defaultExtent = "-25000 250000 280000 860000" +) + +var mapserverDebugLevel = 0 + +func SetDebugLevel(level int) { + if level < 0 || level > 5 { + panic("level must be between 0 and 5") + } + + mapserverDebugLevel = level +} + +func MapWFSToMapfileGeneratorInput(wfs *pdoknlv3.WFS, ownerInfo *smoothoperatorv1.OwnerInfo) (WFSInput, error) { + var metadataID string + if wfs.Spec.Service.Inspire != nil { + metadataID = wfs.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier + } + + extent := defaultExtent + if wfs.Spec.Service.Bbox != nil { + extent = wfs.Spec.Service.Bbox.DefaultCRS.ToExtent() + } + + input := WFSInput{ + BaseServiceInput: BaseServiceInput{ + Title: wfs.Spec.Service.Title, + Abstract: wfs.Spec.Service.Abstract, + Keywords: strings.Join(wfs.Spec.Service.KeywordsIncludingInspireKeyword(), ","), + OnlineResource: wfs.URL().Scheme + "://" + wfs.URL().Host, + Path: wfs.URL().Path, + MetadataID: metadataID, + Extent: extent, + NamespacePrefix: wfs.Spec.Service.Prefix, + NamespaceURI: mapperutils.GetNamespaceURI(wfs.Spec.Service.Prefix, ownerInfo), + AutomaticCasing: wfs.Options().AutomaticCasing, + DataEPSG: wfs.Spec.Service.DefaultCrs, + EPSGList: append([]string{wfs.Spec.Service.DefaultCrs}, wfs.Spec.Service.OtherCrs...), + DebugLevel: mapserverDebugLevel, + AccessConstraints: wfs.Spec.Service.AccessConstraints.String(), + }, + MaxFeatures: strconv.Itoa(smoothoperatorutils.PointerVal(wfs.Spec.Service.CountDefault, defaultMaxFeatures)), + Layers: getWFSLayers(wfs.Spec.Service), + } + + return input, nil +} + +func getWFSLayers(service pdoknlv3.WFSService) (layers []WFSLayer) { + for _, featureType := range service.FeatureTypes { + layer := WFSLayer{ + BaseLayer: BaseLayer{ + Name: featureType.Name, + Title: featureType.Title, + Abstract: featureType.Abstract, + Keywords: strings.Join(featureType.Keywords, ","), + Extent: getWFSExtent(featureType, service), + MetadataID: featureType.DatasetMetadataURL.CSW.MetadataIdentifier, + Columns: getColumns(featureType.Data), + TableName: featureType.Data.GetTableName(), + GeometryType: featureType.Data.GetGeometryType(), + GeopackagePath: getGeopackagePath(featureType.Data.Gpkg), + }, + } + if featureType.Data.Postgis != nil { + layer.Postgis = smoothoperatorutils.Pointer(true) + } + + layers = append(layers, layer) + } + + return +} + +func getWFSExtent(featureType pdoknlv3.FeatureType, service pdoknlv3.WFSService) string { + if featureType.Bbox != nil && featureType.Bbox.DefaultCRS != nil { + return featureType.Bbox.DefaultCRS.ToExtent() + } + if service.Bbox != nil { + return service.Bbox.DefaultCRS.ToExtent() + } + return defaultExtent +} + +func getWMSExtent(serviceLayer pdoknlv3.Layer, serviceExtent string) string { + if len(serviceLayer.BoundingBoxes) > 0 { + return serviceLayer.BoundingBoxes[0].ToExtent() + } + if serviceExtent != "" { + return serviceExtent + } + return defaultExtent +} + +func getColumns(data pdoknlv3.BaseData) []Column { + columns := []Column{{Name: "fuuid"}} + if data.GetColumns() != nil { + for _, column := range *data.GetColumns() { + columns = append(columns, Column{Name: column.Name, Alias: column.Alias}) + } + } else { + return nil + } + return columns +} + +func getGeopackagePath(gpkg *pdoknlv3.Gpkg) *string { + if gpkg == nil { + return nil + } + index := strings.LastIndex(gpkg.BlobKey, "/") + 1 + blobName := gpkg.BlobKey[index:] + return smoothoperatorutils.Pointer(geopackagePath + "/" + blobName) +} + +func MapWMSToMapfileGeneratorInput(wms *pdoknlv3.WMS, ownerInfo *smoothoperatorv1.OwnerInfo) (WMSInput, error) { + service := wms.Spec.Service + + authority := wms.GetAuthority() + authorityURL := "" + datasetOwner := "" + if authority != nil { + authorityURL = authority.URL + datasetOwner = authority.Name + } + + box := service.GetBoundingBox() + extent := box.ToExtent() + + maxSize := "4000" + if service.MaxSize != nil { + maxSize = strconv.Itoa(int(*service.MaxSize)) + } + + metadataID := "" + if service.Inspire != nil { + metadataID = service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier + } + + var fonts *string + + if service.StylingAssets != nil { + writeFonts := mapperutils.AnyMatch(service.StylingAssets.BlobKeys, func(s string) bool { + return strings.HasSuffix(s, ".ttf") + }) + + if writeFonts { + fonts = smoothoperatorutils.Pointer("/srv/data/config/fonts") + } + } + + result := WMSInput{ + BaseServiceInput: BaseServiceInput{ + Title: service.Title, + Abstract: service.Abstract, + Keywords: strings.Join(wms.Spec.Service.KeywordsIncludingInspireKeyword(), ","), + Extent: extent, + NamespacePrefix: wms.Spec.Service.Prefix, + NamespaceURI: mapperutils.GetNamespaceURI(wms.Spec.Service.Prefix, ownerInfo), + OnlineResource: wms.URL().Scheme + "://" + wms.URL().Host, + Path: wms.URL().Path, + MetadataID: metadataID, + DatasetOwner: &datasetOwner, + AuthorityURL: &authorityURL, + AutomaticCasing: wms.Options().AutomaticCasing, + DataEPSG: service.DataEPSG, + AccessConstraints: service.AccessConstraints.String(), + }, + Layers: []WMSLayer{}, + GroupLayers: []GroupLayer{}, + Symbols: getSymbols(wms), + Fonts: fonts, + OutputFormatJpg: "jpg", + OutputFormatPng: "png", + Templates: constants.HTMLTemplatesPath, + MaxSize: maxSize, + } + + if wms.Spec.Service.Layer.Name != nil { + result.TopLevelName = *wms.Spec.Service.Layer.Name + } + + if wms.Spec.Service.Resolution != nil { + result.Resolution = strconv.Itoa(int(*wms.Spec.Service.Resolution)) + } + if wms.Spec.Service.DefResolution != nil { + result.DefResolution = strconv.Itoa(int(*wms.Spec.Service.DefResolution)) + } + + mapLayers(wms, extent, &result) + + return result, nil +} + +func mapLayers(wms *pdoknlv3.WMS, extent string, result *WMSInput) { + epsgs := []string{} + + annotatedLayers := wms.Spec.Service.GetAnnotatedLayers() + for _, annotatedLayer := range annotatedLayers { + + for _, bbox := range annotatedLayer.BoundingBoxes { + if !slices.Contains(epsgs, bbox.CRS) { + epsgs = append(epsgs, bbox.CRS) + } + } + + if annotatedLayer.IsDataLayer { + layer := getWMSLayer(annotatedLayer.Layer, extent, wms) + result.Layers = append(result.Layers, layer) + } else if annotatedLayer.IsGroupLayer && !annotatedLayer.IsTopLayer { + groupLayer := GroupLayer{ + Name: *annotatedLayer.Layer.Name, + Title: smoothoperatorutils.PointerVal(annotatedLayer.Layer.Title, ""), + Abstract: smoothoperatorutils.PointerVal(annotatedLayer.Layer.Abstract, ""), + StyleName: "", + StyleTitle: "", + } + if len(annotatedLayer.Layer.Styles) > 0 { + groupLayer.StyleName = annotatedLayer.Layer.Styles[0].Name + groupLayer.StyleTitle = smoothoperatorutils.PointerVal(annotatedLayer.Layer.Styles[0].Title, "") + } + result.GroupLayers = append(result.GroupLayers, groupLayer) + } + } + + if !slices.Contains(epsgs, "CRS:84") { + epsgs = append(epsgs, "CRS:84") + } + + result.EPSGList = epsgs +} + +func getWMSLayer(serviceLayer pdoknlv3.Layer, serviceExtent string, wms *pdoknlv3.WMS) WMSLayer { + groupName := "" + parent := wms.Spec.Service.GetParentLayer(serviceLayer) + // If the layer falls directly under the toplayer, the groupname is omitted + if !parent.IsTopLayer() && parent.IsGroupLayer() && parent.Name != nil && parent.Visible { + groupName = *parent.Name + } + + var columns []Column + if serviceLayer.Data != nil { + columns = getColumns(serviceLayer.Data.BaseData) + } + + var tableName *string + if serviceLayer.Data != nil { + tableName = serviceLayer.Data.GetTableName() + } + + metadataID := "" + if serviceLayer.DatasetMetadataURL != nil && serviceLayer.DatasetMetadataURL.CSW != nil { + metadataID = serviceLayer.DatasetMetadataURL.CSW.MetadataIdentifier + } + + result := WMSLayer{ + BaseLayer: BaseLayer{ + Name: *serviceLayer.Name, + Title: smoothoperatorutils.PointerVal(serviceLayer.Title, ""), + Abstract: smoothoperatorutils.PointerVal(serviceLayer.Abstract, ""), + Keywords: strings.Join(serviceLayer.Keywords, ","), + Extent: getWMSExtent(serviceLayer, serviceExtent), + MetadataID: metadataID, + Columns: columns, + GeometryType: nil, + GeopackagePath: nil, + TableName: tableName, + Postgis: nil, + MinScale: serviceLayer.MinScaleDenominator, + MaxScale: serviceLayer.MaxScaleDenominator, + LabelNoClip: serviceLayer.LabelNoClip, + }, + GroupName: groupName, + Styles: []Style{}, + Offsite: "", + } + + for _, style := range serviceLayer.Styles { + stylePath := "/styling/" + smoothoperatorutils.PointerVal(style.Visualization, "") + result.Styles = append(result.Styles, Style{ + Path: stylePath, + Title: smoothoperatorutils.PointerVal(style.Title, ""), + }) + } + + if serviceLayer.Data != nil { + SetDataFields(wms, &result, *serviceLayer.Data) + } + + return result +} + +func getSymbols(wms *pdoknlv3.WMS) []string { + result := make([]string, 0) + service := wms.Spec.Service + if service.StylingAssets != nil { + for _, ref := range service.StylingAssets.ConfigMapRefs { + for _, key := range ref.Keys { + if strings.HasSuffix(key, ".symbol") { + result = append(result, "/styling/"+key) + } + } + } + } + return result +} diff --git a/internal/controller/mapfilegenerator/test_data/expected/wfs.json b/internal/controller/mapfilegenerator/test_data/expected/wfs.json new file mode 100644 index 0000000..0855366 --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/expected/wfs.json @@ -0,0 +1,73 @@ +{ + "service_title": "some Service title", + "service_abstract": "some \"Service\" abstract", + "service_keywords": "service-keyword-1,service-keyword-2,infoFeatureAccessService", + "service_extent": "0.0 2.0 1.0 3.0", + "service_wfs_maxfeatures": "1000", + "service_namespace_prefix": "prefix", + "service_namespace_uri": "http://prefix.geonovum.nl", + "service_onlineresource": "https://service.pdok.nl", + "service_path": "/datasetOwner/dataset/theme/wfs/v1_0", + "service_metadata_id": "metameta-meta-meta-meta-metametameta", + "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "automatic_casing": true, + "data_epsg": "EPSG:28992", + "service_debug_level": 0, + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326" + ], + "layers": [ + { + "name": "featuretype-1-name", + "title": "featuretype-1-title", + "abstract": "feature \"1\" abstract", + "keywords": "featuretype-1-keyword-1,featuretype-1-keyword-2", + "layer_extent": "0.0 2.0 1.0 3.0", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "featuretype-1-column-1" + }, + { + "name": "featuretype-1-column-2" + } + ], + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/file-1.gpkg", + "tablename": "featuretype-1" + }, + { + "name": "featuretype-2-name", + "title": "featuretype-2-title", + "abstract": "feature \"2\" abstract", + "keywords": "featuretype-2-keyword-1,featuretype-2-keyword-2", + "layer_extent": "0.0 2.0 1.0 3.0", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "featuretype-2-column-1", + "alias": "alias_featuretype-2-column-1" + }, + { + "name": "featuretype-2-column-2" + } + ], + "geometry_type": "MultiLine", + "tablename": "featuretype-2", + "postgis": true + } + ] +} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_group.json b/internal/controller/mapfilegenerator/test_data/expected/wms_group.json new file mode 100644 index 0000000..57a00d4 --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/expected/wms_group.json @@ -0,0 +1,468 @@ +{ + "authority_url": "https://www.hetwaterschapshuis.nl/", + "automatic_casing": true, + "data_epsg": "EPSG:28992", + "dataset_owner": "hwh", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "group_layers": [ + { + "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", + "name": "HY.PhysicalWaters.ManMadeObject", + "style_name": "HY.PhysicalWaters.ManMadeObject.Default", + "style_title": "Man-made objects default style", + "title": "Man-made Object" + } + ], + "layers": [ + { + "abstract": "Watercourse", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "condition" + }, + { + "name": "delineationknown" + }, + { + "name": "endlifespanversion" + }, + { + "name": "length" + }, + { + "name": "level" + }, + { + "name": "localid" + }, + { + "name": "localtype" + }, + { + "name": "name" + }, + { + "name": "namespace" + }, + { + "name": "origin" + }, + { + "name": "persistence" + }, + { + "name": "streamorder" + }, + { + "name": "tidal" + }, + { + "name": "widthrange" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "MultiLineString", + "gpkg_path": "/srv/data/gpkg/Waterbody.gpkg", + "keywords": "Hydroobject,Waterbody,Watercourse,River,Stream,Lake,Reservoir", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.Waterbodies", + "styles": [ + { + "path": "/styling/watercourse.style", + "title": "Water bodies default style" + } + ], + "tablename": "watercourse", + "title": "Waterbody" + }, + { + "abstract": "Drainage basin", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "area" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "endlifespanversion" + }, + { + "name": "localid" + }, + { + "name": "name" + }, + { + "name": "namespace" + }, + { + "name": "order" + }, + { + "name": "orderscheme" + }, + { + "name": "origin" + }, + { + "name": "outlet" + }, + { + "name": "scope" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "MultiPolygon", + "gpkg_path": "/srv/data/gpkg/Catchment.gpkg", + "keywords": "AfvoergebiedAanvoergebied,Catchment,Basin,Catchment Area,Drainage basin", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.Catchments", + "styles": [ + { + "path": "/styling/drainagebasin.style", + "title": "Drainage Basin default style" + } + ], + "tablename": "drainagebasin", + "title": "Catchment" + }, + { + "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "condition" + }, + { + "name": "endlifespanversion" + }, + { + "name": "localid" + }, + { + "name": "name" + }, + { + "name": "namespace" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", + "group_name": "HY.PhysicalWaters.ManMadeObject", + "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.ManMadeObject.lock", + "styles": [ + { + "path": "/styling/lock.style", + "title": "Lock" + } + ], + "tablename": "lock", + "title": "Man-made Object" + }, + { + "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "condition" + }, + { + "name": "endlifespanversion" + }, + { + "name": "localid" + }, + { + "name": "name" + }, + { + "name": "namespace" + }, + { + "name": "type" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", + "group_name": "HY.PhysicalWaters.ManMadeObject", + "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.ManMadeObject.crossingpoint", + "styles": [ + { + "path": "/styling/crossingpoint.style", + "title": "Crossing Point" + } + ], + "tablename": "crossingpoint", + "title": "Man-made Object" + }, + { + "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "condition" + }, + { + "name": "endlifespanversion" + }, + { + "name": "localid" + }, + { + "name": "name" + }, + { + "name": "namespace" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", + "group_name": "HY.PhysicalWaters.ManMadeObject", + "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.ManMadeObject.sluice", + "styles": [ + { + "path": "/styling/sluice.style", + "title": "Sluice" + } + ], + "tablename": "sluice", + "title": "Man-made Object" + }, + { + "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "condition" + }, + { + "name": "endlifespanversion" + }, + { + "name": "localid" + }, + { + "name": "name" + }, + { + "name": "namespace" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "MultiLineString", + "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", + "group_name": "HY.PhysicalWaters.ManMadeObject", + "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.ManMadeObject.embankment", + "styles": [ + { + "path": "/styling/embankment.style", + "title": "Embankment" + } + ], + "tablename": "embankment", + "title": "Man-made Object" + }, + { + "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "condition" + }, + { + "name": "endlifespanversion" + }, + { + "name": "localid" + }, + { + "name": "name" + }, + { + "name": "namespace" + }, + { + "name": "type" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "MultiLineString", + "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", + "group_name": "HY.PhysicalWaters.ManMadeObject", + "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.ManMadeObject.crossingline", + "styles": [ + { + "path": "/styling/crossingline.style", + "title": "Crossing Line" + } + ], + "tablename": "crossingline", + "title": "Man-made Object" + }, + { + "abstract": "Crossing line, crossing point, dam or weir, sluice, embankment, lock.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "gml_id" + }, + { + "name": "beginlifespanversion" + }, + { + "name": "condition" + }, + { + "name": "endlifespanversion" + }, + { + "name": "localid" + }, + { + "name": "name" + }, + { + "name": "namespace" + } + ], + "dataset_metadata_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "dataset_source_id": "07575774-57a1-4419-bab4-6c88fdeb02b2", + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/Man-madeObject.gpkg", + "group_name": "HY.PhysicalWaters.ManMadeObject", + "keywords": "Man Made,Duikersifonhevel,Aquaduct,Brug,Stuw,Vastedam,Waterkering,Sluis.", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "HY.PhysicalWaters.ManMadeObject.damorweir", + "styles": [ + { + "path": "/styling/damorweir.style", + "title": "Dam or Weir" + } + ], + "tablename": "damorweir", + "title": "Man-made Object" + } + ], + "maxSize": "4000", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "service_abstract": "Deze dataset is gebaseerd op (niet geharmoniseerde) data van alle waterschappen in Nederland conform INSPIRE. De dataset bevat de volgende INSPIRE objecten: CrossingLine, CrossingPoint, DamOrWeir, DrainageBasin, Embankment, Lock, Sluice, Watercourse.", + "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "service_keywords": "Aquaduct,Brug,Duikersifonhevel,Stuw,Vastedam,AfvoerAanvoergebied,Waterkering,Sluis,HydroObject,HVD,Aardobservatie en milieu,infoMapAccessService", + "service_metadata_id": "871a58f8-c9f1-41a4-be37-0f059e0f886f", + "service_namespace_prefix": "hydrografie", + "service_namespace_uri": "http://hydrografie.geonovum.nl", + "service_onlineresource": "https://service.pdok.nl", + "service_path": "/hwh/hydrografie/wms/v2_0", + "service_title": "Waterschappen Hydrografie (INSPIRE geharmoniseerd) WMS", + "symbols": [ + "/styling/bridge.symbol", + "/styling/x.symbol" + ], + "templates": "/srv/data/config/templates" +} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json b/internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json new file mode 100644 index 0000000..5a6f8fe --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/expected/wms_group_and_toplayer.json @@ -0,0 +1,616 @@ +{ + "authority_url": "https://www.kadaster.nl", + "automatic_casing": true, + "data_epsg": "EPSG:28992", + "dataset_owner": "kadaster", + "defresolution": "91", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "fonts": "/srv/data/config/fonts", + "group_layers": [ + { + "abstract": "De laag Bebouwing is een selectie op panden van de BGT.", + "name": "Bebouwing", + "style_name": "standaard:bebouwing", + "style_title": "Standaardvisualisatie Bebouwing", + "title": "Bebouwing" + }, + { + "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", + "name": "Perceel", + "style_name": "standaard:perceel", + "style_title": "Standaardvisualisatie Perceel", + "title": "Perceel" + } + ], + "layers": [ + { + "abstract": "De laag Bebouwing is een selectie op panden van de BGT.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "object_begin_tijd" + }, + { + "alias": "LV-publicatiedatum", + "name": "lv_publicatiedatum" + }, + { + "name": "relatieve_hoogteligging" + }, + { + "name": "in_onderzoek" + }, + { + "name": "tijdstip_registratie" + }, + { + "name": "identificatie_namespace" + }, + { + "alias": "identificatieLokaalID", + "name": "identificatie_lokaal_id" + }, + { + "name": "bronhouder" + }, + { + "alias": "bgt-status", + "name": "bgt_status" + }, + { + "alias": "plus-status", + "name": "plus_status" + }, + { + "alias": "identificatieBAGPND", + "name": "identificatie_bag_pnd" + } + ], + "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", + "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", + "geometry_type": "Polygon", + "gpkg_path": "/srv/data/gpkg/pand.gpkg", + "group_name": "Bebouwing", + "keywords": "Bebouwing", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "6001", + "minscale": "50", + "name": "Bebouwingvlak", + "styles": [ + { + "path": "/styling/bebouwing.style", + "title": "Standaardvisualisatie" + }, + { + "path": "/styling/bebouwing_kwaliteit.style", + "title": "Kwaliteitsvisualisatie" + }, + { + "path": "/styling/bebouwing_print.style", + "title": "Printvisualisatie" + }, + { + "path": "/styling/bebouwing.group.style", + "title": "Standaardvisualisatie Bebouwing" + }, + { + "path": "/styling/bebouwing_kwaliteit.group.style", + "title": "Kwaliteitsvisualisatie Bebouwing" + }, + { + "path": "/styling/bebouwing_print.group.style", + "title": "Printvisualisatie Bebouwing" + } + ], + "tablename": "pand", + "title": "Bebouwingvlak" + }, + { + "abstract": "De laag Bebouwing is een selectie op panden van de BGT.", + "columns": [ + { + "name": "fuuid" + }, + { + "alias": "bebouwingID", + "name": "bebouwing_id" + }, + { + "name": "hoek" + }, + { + "name": "tekst" + }, + { + "alias": "identificatie_BAGVBOLaagsteHuisnummer", + "name": "bag_vbo_laagste_huisnummer" + }, + { + "alias": "identificatie_BAGVBOHoogsteHuisnummer", + "name": "bag_vbo_hoogste_huisnummer" + }, + { + "name": "hoek" + } + ], + "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", + "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/pand_nummeraanduiding.gpkg", + "group_name": "Bebouwing", + "keywords": "Nummeraanduidingreeks", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "2001", + "minscale": "50", + "name": "Nummeraanduidingreeks", + "styles": [ + { + "path": "/styling/nummeraanduidingreeks.style", + "title": "Standaardvisualisatie" + }, + { + "path": "/styling/nummeraanduidingreeks_kwaliteit.style", + "title": "Kwaliteitsvisualisatie" + }, + { + "path": "/styling/nummeraanduidingreeks_print.style", + "title": "Printvisualisatie" + }, + { + "path": "/styling/nummeraanduidingreeks.group.style", + "title": "Standaardvisualisatie Bebouwing" + }, + { + "path": "/styling/nummeraanduidingreeks_kwaliteit.group.style", + "title": "Kwaliteitsvisualisatie Bebouwing" + }, + { + "path": "/styling/nummeraanduidingreeks_print.group.style", + "title": "Printvisualisatie Bebouwing" + } + ], + "tablename": "pand_nummeraanduiding", + "title": "Nummeraanduidingreeks" + }, + { + "abstract": "De laag Openbareruimtenaam is een selectie op de openbare ruimte labels van de BGT met een bgt-status \"bestaand\" die een classificatie (openbareruimtetype) Weg en Water hebben.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "object_begin_tijd" + }, + { + "alias": "LV-publicatiedatum", + "name": "lv_publicatiedatum" + }, + { + "name": "relatieve_hoogteligging" + }, + { + "name": "in_onderzoek" + }, + { + "name": "tijdstip_registratie" + }, + { + "name": "identificatie_namespace" + }, + { + "alias": "identificatieLokaalID", + "name": "identificatie_lokaal_id" + }, + { + "name": "bronhouder" + }, + { + "alias": "bgt-status", + "name": "bgt_status" + }, + { + "alias": "plus-status", + "name": "plus_status" + }, + { + "alias": "identificatieBAGOPR", + "name": "identificatie_bag_opr" + }, + { + "name": "tekst" + }, + { + "name": "hoek" + }, + { + "name": "openbare_ruimte_type" + } + ], + "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", + "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/openbareruimtelabel.gpkg", + "keywords": "Openbare ruimte naam", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "2001", + "minscale": "50", + "name": "OpenbareRuimteNaam", + "styles": [ + { + "path": "/styling/openbareruimtenaam.style", + "title": "Standaardvisualisatie" + }, + { + "path": "/styling/openbareruimtenaam_kwaliteit.style", + "title": "Kwaliteitsvisualisatie" + }, + { + "path": "/styling/openbareruimtenaam_print.style", + "title": "Printvisualisatie" + }, + { + "path": "/styling/openbareruimtenaam.group.style", + "title": "Standaardvisualisatie OpenbareRuimteNaam" + }, + { + "path": "/styling/openbareruimtenaam_kwaliteit.group.style", + "title": "Kwaliteitsvisualisatie OpenbareRuimteNaam" + }, + { + "path": "/styling/openbareruimtenaam_print.group.style", + "title": "Printvisualisatie OpenbareRuimteNaam" + } + ], + "tablename": "openbareruimtelabel", + "title": "OpenbareRuimteNaam" + }, + { + "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "identificatie_namespace" + }, + { + "alias": "identificatieLokaalID", + "name": "identificatie_lokaal_id" + }, + { + "name": "begin_geldigheid" + }, + { + "name": "tijdstip_registratie" + }, + { + "name": "volgnummer" + }, + { + "name": "status_historie_code" + }, + { + "name": "status_historie_waarde" + }, + { + "name": "kadastrale_gemeente_code" + }, + { + "name": "kadastrale_gemeente_waarde" + }, + { + "name": "sectie" + }, + { + "alias": "AKRKadastraleGemeenteCodeCode", + "name": "akr_kadastrale_gemeente_code_code" + }, + { + "alias": "AKRKadastraleGemeenteCodeWaarde", + "name": "akr_kadastrale_gemeente_code_waarde" + }, + { + "name": "kadastrale_grootte_waarde" + }, + { + "name": "soort_grootte_code" + }, + { + "name": "soort_grootte_waarde" + }, + { + "name": "perceelnummer" + }, + { + "name": "perceelnummer_rotatie" + }, + { + "name": "perceelnummer_verschuiving_delta_x" + }, + { + "name": "perceelnummer_verschuiving_delta_y" + }, + { + "name": "perceelnummer_plaatscoordinaat_x" + }, + { + "name": "perceelnummer_plaatscoordinaat_y" + } + ], + "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", + "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", + "geometry_type": "Polygon", + "gpkg_path": "/srv/data/gpkg/perceel.gpkg", + "group_name": "Perceel", + "keywords": "Kadastrale percelen", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "6001", + "minscale": "50", + "name": "Perceelvlak", + "styles": [ + { + "path": "/styling/perceelvlak.style", + "title": "Standaardvisualisatie" + }, + { + "path": "/styling/perceelvlak_kwaliteit.style", + "title": "Kwaliteitsvisualisatie" + }, + { + "path": "/styling/perceelvlak_print.style", + "title": "Printvisualisatie" + }, + { + "path": "/styling/perceelvlak.group.style", + "title": "Standaardvisualisatie Perceel" + }, + { + "path": "/styling/perceelvlak_kwaliteit.group.style", + "title": "Kwaliteitsvisualisatie Perceel" + }, + { + "path": "/styling/perceelvlak_print.group.style", + "title": "Printvisualisatie Perceel" + } + ], + "tablename": "perceel", + "title": "Perceelvlak" + }, + { + "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", + "columns": [ + { + "name": "fuuid" + }, + { + "alias": "perceelID", + "name": "perceel_id" + }, + { + "name": "perceelnummer" + }, + { + "name": "rotatie" + }, + { + "name": "verschuiving_delta_x" + }, + { + "name": "verschuiving_delta_y" + } + ], + "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", + "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/perceel_label.gpkg", + "group_name": "Perceel", + "keywords": "Kadastrale percelen", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "6001", + "minscale": "50", + "name": "Label", + "styles": [ + { + "path": "/styling/label.style", + "title": "Standaardvisualisatie" + }, + { + "path": "/styling/label.group.style", + "title": "Standaardvisualisatie Perceel" + }, + { + "path": "/styling/label_kwaliteit.style", + "title": "Kwaliteitsvisualisatie" + }, + { + "path": "/styling/label_kwaliteit.group.style", + "title": "Kwaliteitsvisualisatie Perceel" + }, + { + "path": "/styling/label_print.style", + "title": "Printvisualisatie" + }, + { + "path": "/styling/label_print.group.style", + "title": "Printvisualisatie Perceel" + } + ], + "tablename": "perceel_label", + "title": "Label" + }, + { + "abstract": "Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal geรฏdentificeerd is en met kadastrale grenzen begrensd is.", + "columns": [ + { + "name": "fuuid" + }, + { + "alias": "perceelID", + "name": "perceel_id" + } + ], + "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", + "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", + "geometry_type": "LineString", + "gpkg_path": "/srv/data/gpkg/perceel_bijpijling.gpkg", + "group_name": "Perceel", + "keywords": "Kadastrale percelen", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "6001", + "minscale": "50", + "name": "Bijpijling", + "styles": [ + { + "path": "/styling/bijpijling.style", + "title": "Standaardvisualisatie" + }, + { + "path": "/styling/bijpijling_kwaliteit.style", + "title": "Kwaliteitsvisualisatie" + }, + { + "path": "/styling/bijpijling_print.style", + "title": "Printvisualisatie" + }, + { + "path": "/styling/bijpijling.group.style", + "title": "Standaardvisualisatie Perceel" + }, + { + "path": "/styling/bijpijling_kwaliteit.group.style", + "title": "Kwaliteitsvisualisatie Perceel" + }, + { + "path": "/styling/bijpijling_print.group.style", + "title": "Printvisualisatie Perceel" + } + ], + "tablename": "perceel_bijpijling", + "title": "Bijpijling" + }, + { + "abstract": "Een Kadastrale Grens is de weergave van een grens op de kadastrale kaart die door de dienst van het Kadaster tussen percelen (voorlopig) vastgesteld wordt, op basis van inlichtingen van belanghebbenden en met gebruikmaking van de aan de kadastrale kaart ten grondslag liggende bescheiden die in elk geval de landmeetkundige gegevens bevatten van hetgeen op die kaart wordt weergegeven.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "begin_geldigheid" + }, + { + "name": "tijdstip_registratie" + }, + { + "name": "volgnummer" + }, + { + "name": "status_historie_code" + }, + { + "name": "status_historie_waarde" + }, + { + "name": "identificatie_namespace" + }, + { + "alias": "identificatieLokaalID", + "name": "identificatie_lokaal_id" + }, + { + "name": "type_grens_code" + }, + { + "name": "type_grens_waarde" + }, + { + "alias": "ClassificatieKwaliteitCode", + "name": "classificatie_kwaliteit_code" + }, + { + "alias": "ClassificatieKwaliteitWaarde", + "name": "classificatie_kwaliteit_waarde" + }, + { + "name": "perceel_links_identificatie_namespace" + }, + { + "alias": "perceelLinksIdentificatieLokaalID", + "name": "perceel_links_identificatie_lokaal_id" + }, + { + "name": "perceel_rechts_identificatie_namespace" + }, + { + "alias": "perceelRechtsIdentificatieLokaalID", + "name": "perceel_rechts_identificatie_lokaal_id" + } + ], + "dataset_metadata_id": "a29917b9-3426-4041-a11b-69bcb2256904", + "dataset_source_id": "06b6c650-cdb1-11dd-ad8b-0800200c9a64", + "geometry_type": "LineString", + "gpkg_path": "/srv/data/gpkg/kadastrale_grens.gpkg", + "keywords": "Grens,Kadastrale grenzen", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "6001", + "minscale": "50", + "name": "KadastraleGrens", + "styles": [ + { + "path": "/styling/kadastralegrens.style", + "title": "Standaardvisualisatie" + }, + { + "path": "/styling/kadastralegrens_kwaliteit.style", + "title": "Kwaliteitsvisualisatie" + }, + { + "path": "/styling/kadastralegrens_print.style", + "title": "Printvisualisatie" + }, + { + "path": "/styling/kadastralegrens.group.style", + "title": "Standaardvisualisatie KadastraleGrens" + }, + { + "path": "/styling/kadastralegrens_kwaliteit.group.style", + "title": "Kwaliteitsvisualisatie KadastraleGrens" + }, + { + "path": "/styling/kadastralegrens_print.group.style", + "title": "Printvisualisatie KadastraleGrens" + } + ], + "tablename": "kadastrale_grens", + "title": "KadastraleGrens" + } + ], + "maxSize": "10000", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "resolution": "91", + "service_abstract": "Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen informatie kan vastleggen en presenteren.", + "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-25000 250000 280000 860000", + "service_keywords": "Kadaster,Kadastrale percelen,Kadastrale grens,Kadastrale kaart,Bebouwing,Nummeraanduidingreeks,Openbare ruimte naam,Perceel,Grens,Kwaliteit,Kwaliteitslabels,HVD,Geospatiale data", + "service_metadata_id": "", + "service_namespace_prefix": "kadastralekaart", + "service_namespace_uri": "http://kadastralekaart.geonovum.nl", + "service_onlineresource": "https://service.pdok.nl", + "service_path": "/kadaster/kadastralekaart/wms/v5_0", + "service_title": "Kadastrale Kaart (WMS)", + "symbols": [], + "templates": "/srv/data/config/templates", + "top_level_name": "Kadastralekaart" +} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json b/internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json new file mode 100644 index 0000000..1e92343 --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/expected/wms_groupless.json @@ -0,0 +1,283 @@ +{ + "authority_url": "https://www.rijkswaterstaat.nl", + "automatic_casing": true, + "data_epsg": "EPSG:28992", + "dataset_owner": "rws", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "fonts": "/srv/data/config/fonts", + "group_layers": [], + "layers": [ + { + "abstract": "Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, routenummer, wegbeheerder, huisnummers, enz. weer.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "objectid" + }, + { + "name": "wvk_id" + }, + { + "name": "wvk_begdat" + }, + { + "name": "jte_id_beg" + }, + { + "name": "jte_id_end" + }, + { + "name": "wegbehsrt" + }, + { + "name": "wegnummer" + }, + { + "name": "wegdeelltr" + }, + { + "name": "hecto_lttr" + }, + { + "name": "bst_code" + }, + { + "name": "rpe_code" + }, + { + "name": "admrichtng" + }, + { + "name": "rijrichtng" + }, + { + "name": "stt_naam" + }, + { + "name": "stt_bron" + }, + { + "name": "wpsnaam" + }, + { + "name": "gme_id" + }, + { + "name": "gme_naam" + }, + { + "name": "hnrstrlnks" + }, + { + "name": "hnrstrrhts" + }, + { + "name": "e_hnr_lnks" + }, + { + "name": "e_hnr_rhts" + }, + { + "name": "l_hnr_lnks" + }, + { + "name": "l_hnr_rhts" + }, + { + "name": "begafstand" + }, + { + "name": "endafstand" + }, + { + "name": "beginkm" + }, + { + "name": "eindkm" + }, + { + "name": "pos_tv_wol" + }, + { + "name": "wegbehcode" + }, + { + "name": "wegbehnaam" + }, + { + "name": "distrcode" + }, + { + "name": "distrnaam" + }, + { + "name": "dienstcode" + }, + { + "name": "dienstnaam" + }, + { + "name": "wegtype" + }, + { + "name": "wgtype_oms" + }, + { + "name": "routeltr" + }, + { + "name": "routenr" + }, + { + "name": "routeltr2" + }, + { + "name": "routenr2" + }, + { + "name": "routeltr3" + }, + { + "name": "routenr3" + }, + { + "name": "routeltr4" + }, + { + "name": "routenr4" + }, + { + "name": "wegnr_aw" + }, + { + "name": "wegnr_hmp" + }, + { + "name": "geobron_id" + }, + { + "name": "geobron_nm" + }, + { + "name": "bronjaar" + }, + { + "name": "openlr" + }, + { + "name": "bag_orl" + }, + { + "name": "frc" + }, + { + "name": "fow" + }, + { + "name": "alt_naam" + }, + { + "name": "alt_nr" + }, + { + "name": "rel_hoogte" + }, + { + "name": "st_lengthshape" + } + ], + "dataset_metadata_id": "a9b7026e-0a81-4813-93bd-ba49e6f28502", + "dataset_source_id": "8f0497f0-dbd7-4bee-b85a-5fdec484a7ff", + "geometry_type": "MultiLineString", + "gpkg_path": "/srv/data/gpkg/nwb_wegen.gpkg", + "keywords": "Vervoersnetwerken,Menselijke gezondheid en veiligheid,Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai),Nationaal,Voertuigen,Verkeer,Wegvakken", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "wegvakken", + "styles": [ + { + "path": "/styling/wegvakken.style", + "title": "NWB - Wegvakken" + } + ], + "tablename": "wegvakken", + "title": "Wegvakken" + }, + { + "abstract": "Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, zijde en hectoletter weer.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "objectid" + }, + { + "name": "hectomtrng" + }, + { + "name": "afstand" + }, + { + "name": "wvk_id" + }, + { + "name": "wvk_begdat" + }, + { + "name": "zijde" + }, + { + "name": "hecto_lttr" + } + ], + "dataset_metadata_id": "a9b7026e-0a81-4813-93bd-ba49e6f28502", + "dataset_source_id": "8f0497f0-dbd7-4bee-b85a-5fdec484a7ff", + "geometry_type": "MultiPoint", + "gpkg_path": "/srv/data/gpkg/nwb_wegen.gpkg", + "keywords": "Vervoersnetwerken,Menselijke gezondheid en veiligheid,Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai),Nationaal,Voertuigen,Verkeer,Hectometerpunten", + "layer_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "maxscale": "50000", + "minscale": "1", + "name": "hectopunten", + "styles": [ + { + "path": "/styling/hectopunten.style", + "title": "NWB - Hectopunten" + } + ], + "tablename": "hectopunten", + "title": "Hectopunten" + } + ], + "maxSize": "4000", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "service_abstract": "Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien van een straatnaam of nummer.", + "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961", + "service_keywords": "Vervoersnetwerken,Menselijke gezondheid en veiligheid,Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai),Nationaal,Voertuigen,Verkeer,Wegvakken,Hectometerpunten,HVD,Mobiliteit,infoMapAccessService", + "service_metadata_id": "f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8", + "service_namespace_prefix": "nwbwegen", + "service_namespace_uri": "http://nwbwegen.geonovum.nl", + "service_onlineresource": "https://service.pdok.nl", + "service_path": "/rws/nwbwegen/wms/v1_0", + "service_title": "NWB - Wegen WMS", + "symbols": [ + "/styling/nwb_wegen_hectopunten.symbol" + ], + "templates": "/srv/data/config/templates" +} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json b/internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json new file mode 100644 index 0000000..a8ff834 --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/expected/wms_postgis.json @@ -0,0 +1,93 @@ +{ + "authority_url": "http://www.brt.nl", + "automatic_casing": true, + "data_epsg": "EPSG:28992", + "dataset_owner": "brt", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "group_layers": [], + "layers": [ + { + "abstract": "Alle recente BRT terugmeldingen gedaan door BRT gebruikers.", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "meldingsnummer_volledig" + }, + { + "name": "tijdstip_registratie" + }, + { + "name": "status" + }, + { + "name": "omschrijving" + }, + { + "name": "bronhoudercode" + }, + { + "name": "bronhoudernaam" + }, + { + "name": "tijdstip_statuswijziging" + }, + { + "name": "toelichting" + }, + { + "name": "objectid" + }, + { + "name": "objecttype" + }, + { + "name": "hoogte_vanaf_maaiveld" + } + ], + "dataset_metadata_id": "7a84c4de-4ec0-4202-a8d0-792fb7d39d1f", + "dataset_source_id": "07c7d650-cdb1-11dd-ad8b-0800200c9a60", + "geometry_type": "Point", + "keywords": "brtterugmeldingen", + "layer_extent": "-7000 289000 300000 629000", + "name": "brtterugmeldingen", + "postgis": true, + "styles": [ + { + "path": "/styling/terugmeldingen.style", + "title": "Terugmeldingen" + } + ], + "tablename": "brtterugmeldingen.brtterugmeldingen_v1", + "title": "BRT Terugmeldingen" + } + ], + "maxSize": "4000", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "service_abstract": "De BRT terugmeldingenservice bevat alle recente meldingen op BRT objecten waar twijfel over de juistheid bestaat. Zowel terugmeldingen op de TOP10 als meldingen die gemaakt zijn op de gegeneraliseerde kaartproducten (TOP25, TOP50, TOP100, TOP250) worden hierin geregistreerd. Daarnaast kan je de inhoud en status van de meldingen inzien. Ook een vermoedelijke fout geconstateerd? Doe een melding op https://verbeterdekaart.kadaster.nl", + "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-7000 289000 300000 629000", + "service_keywords": "Basisregistratie Topografie,BRT,terugmeldingen,TOP10NL,TOP25,TOP50,TOP100,TOP250,in onderzoek register,verbeter de kaart,verbeterdekaart", + "service_metadata_id": "", + "service_namespace_prefix": "terugmeldingen", + "service_namespace_uri": "http://terugmeldingen.geonovum.nl", + "service_onlineresource": "https://service.pdok.nl", + "service_path": "/brt/terugmeldingen/wms/v1_0", + "service_title": "BRT Terugmeldingen WMS", + "symbols": [ + "/styling/terugmeldingen.symbol" + ], + "templates": "/srv/data/config/templates" +} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/expected/wms_tif.json b/internal/controller/mapfilegenerator/test_data/expected/wms_tif.json new file mode 100644 index 0000000..99abb1b --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/expected/wms_tif.json @@ -0,0 +1,306 @@ +{ + "authority_url": "http://www.kadaster.nl", + "automatic_casing": false, + "data_epsg": "EPSG:28992", + "dataset_owner": "kadaster", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "group_layers": [ + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "name": "lufolabels", + "style_name": "luchtfotolabels", + "style_title": "Luchtfotolabels", + "title": "Luchtfoto labels" + } + ], + "layers": [ + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "48001", + "minscale": "24001", + "name": "luchtfotoroads_100pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/roads.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/100pixkm_luforoads.vrt", + "title": "Luchtfoto roads 100pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "24001", + "minscale": "12001", + "name": "luchtfotoroads_200pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "2.0", + "styles": [ + { + "path": "/styling/roads.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/200pixkm_luforoads.vrt", + "title": "Luchtfoto roads 200pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "12001", + "minscale": "6001", + "name": "luchtfotoroads_400pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "2.5", + "styles": [ + { + "path": "/styling/roads.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/400pixkm_luforoads.vrt", + "title": "Luchtfoto roads 400pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "6001", + "minscale": "3001", + "name": "luchtfotoroads_800pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/roads.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/800pixkm_luforoads.vrt", + "title": "Luchtfoto roads 800pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "3001", + "minscale": "1501", + "name": "luchtfotoroads_1600pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/roads.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/1600pixkm_luforoads.vrt", + "title": "Luchtfoto roads 1600pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "48001", + "minscale": "24001", + "name": "luchtfotolabels_100pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/labels.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/100pixkm_lufolabels.vrt", + "title": "Luchtfoto labels 100pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "24001", + "minscale": "12001", + "name": "luchtfotolabels_200pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/labels.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/200pixkm_lufolabels.vrt", + "title": "Luchtfoto labels 200pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "12001", + "minscale": "6001", + "name": "luchtfotolabels_400pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/labels.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/400pixkm_lufolabels.vrt", + "title": "Luchtfoto labels 400pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "6001", + "minscale": "3001", + "name": "luchtfotolabels_800pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/labels.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/800pixkm_lufolabels.vrt", + "title": "Luchtfoto labels 800pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "3001", + "minscale": "1501", + "name": "luchtfotolabels_1600pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/labels.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/1600pixkm_lufolabels.vrt", + "title": "Luchtfoto labels 1600pixkm" + }, + { + "abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "dataset_metadata_id": "6ca22f53-b072-42f4-b920-104c7c83cd28", + "dataset_source_id": "901647c2-802d-11e6-ae22-56b6b6499611", + "geometry_type": "Raster", + "get_feature_info_includes_class": false, + "group_name": "lufolabels", + "keywords": "bzk,luchtfotolabels", + "layer_extent": "-25000 250000 280000 860000", + "maxscale": "1501", + "name": "luchtfotolabels_3200pixkm", + "offsite": "#978E97", + "resample": "BILINEAR", + "oversample_ratio": "1", + "styles": [ + { + "path": "/styling/labels.style", + "title": "Luchtfotolabels" + } + ], + "tif_path": "/srv/data/tif/3200pixkm_lufolabels.vrt", + "title": "Luchtfoto labels 3200pixkm" + } + ], + "maxSize": "4000", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "service_abstract": "De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto.", + "service_accessconstraints": "https://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-25000 250000 280000 860000", + "service_keywords": "bzk,luchtfotolabels", + "service_metadata_id": "", + "service_namespace_prefix": "luchtfotolabels", + "service_namespace_uri": "http://luchtfotolabels.geonovum.nl", + "service_onlineresource": "https://service.pdok.nl", + "service_path": "/bzk/luchtfotolabels/wms/v1_0", + "service_title": "Luchtfoto Labels WMS", + "symbols": [], + "templates": "/srv/data/config/templates" +} \ No newline at end of file diff --git a/internal/controller/mapfilegenerator/test_data/input/wfs.yaml b/internal/controller/mapfilegenerator/test_data/input/wfs.yaml new file mode 100644 index 0000000..e7ca80b --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/input/wfs.yaml @@ -0,0 +1,94 @@ +metadata: + labels: + dataset: dataset + dataset-owner: datasetOwner + service-version: v1_0 + theme: theme +spec: + options: + automaticCasing: true + disableWebserviceProxy: false + includeIngress: false + prefetchData: false + rewriteGroupToDataLayers: false + validateChildStyleNameEqual: false + validateRequests: false + service: + abstract: some "Service" abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + bbox: + defaultCRS: + maxx: "1.0" + maxy: "3.0" + minx: "0.0" + miny: "2.0" + defaultCrs: EPSG:28992 + featureTypes: + - abstract: feature "1" abstract + bbox: + defaultCRS: + maxx: "1.0" + maxy: "3.0" + minx: "0.0" + miny: "2.0" + data: + gpkg: + blobKey: public/testme/gpkg/file-1.gpkg + columns: + - name: featuretype-1-column-1 + - name: featuretype-1-column-2 + geometryType: Point + tableName: featuretype-1 + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - featuretype-1-keyword-1 + - featuretype-1-keyword-2 + name: featuretype-1-name + title: featuretype-1-title + - abstract: feature "2" abstract + bbox: + defaultCRS: + maxx: "1.0" + maxy: "3.0" + minx: "0.0" + miny: "2.0" + data: + postgis: + columns: + - alias: alias_featuretype-2-column-1 + name: featuretype-2-column-1 + - name: featuretype-2-column-2 + geometryType: MultiLine + tableName: featuretype-2 + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - featuretype-2-keyword-1 + - featuretype-2-keyword-2 + name: featuretype-2-name + title: featuretype-2-title + inspire: + language: "" + serviceMetadataUrl: + csw: + metadataIdentifier: metameta-meta-meta-meta-metametameta + spatialDatasetIdentifier: "" + keywords: + - service-keyword-1 + - service-keyword-2 + - infoFeatureAccessService + otherCrs: + - EPSG:25831 + - EPSG:25832 + - EPSG:3034 + - EPSG:3035 + - EPSG:3857 + - EPSG:4258 + - EPSG:4326 + ownerInfoRef: "" + prefix: prefix + title: some Service title + url: "https://service.pdok.nl/datasetOwner/dataset/theme/wfs/v1_0" diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_group.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_group.yaml new file mode 100644 index 0000000..20e2937 --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/input/wms_group.yaml @@ -0,0 +1,576 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + lifecycle-phase: prod + service-bundle-id: d30bdf62-1c12-45a5-a57d-367e642ef118 + creationTimestamp: null + labels: + dataset: hydrografie + dataset-owner: hwh + service-type: wms + service-version: v2_0 + name: hwh-hydrografie-v2-0 +spec: + options: + automaticCasing: true + disableWebserviceProxy: false + includeIngress: true + prefetchData: true + rewriteGroupToDataLayers: false + validateChildStyleNameEqual: false + validateRequests: true + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 1544Mi + requests: + ephemeral-storage: 1544Mi + service: + abstract: 'Deze dataset is gebaseerd op (niet geharmoniseerde) data van alle waterschappen + in Nederland conform INSPIRE. De dataset bevat de volgende INSPIRE objecten: + CrossingLine, CrossingPoint, DamOrWeir, DrainageBasin, Embankment, Lock, Sluice, + Watercourse.' + accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + inspire: + language: dut + serviceMetadataUrl: + csw: + metadataIdentifier: 871a58f8-c9f1-41a4-be37-0f059e0f886f + keywords: + - Aquaduct + - Brug + - Duikersifonhevel + - Stuw + - Vastedam + - AfvoerAanvoergebied + - Waterkering + - Sluis + - HydroObject + - HVD + - Aardobservatie en milieu + layer: + abstract: 'Deze dataset is gebaseerd op (niet geharmoniseerde) data van alle + waterschappen in Nederland conform INSPIRE. De dataset bevat de volgende INSPIRE + objecten: CrossingLine, CrossingPoint, DamOrWeir, DrainageBasin, Embankment, + Lock, Sluice, Watercourse.' + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + - bbox: + maxx: "795163" + maxy: "6181970" + minx: "-470271" + miny: "5562310" + crs: EPSG:25831 + - bbox: + maxx: "397827" + maxy: "6190420" + minx: "62461.6" + miny: "5565550" + crs: EPSG:25832 + - bbox: + maxx: "3220070" + maxy: "3840030" + minx: "2613360" + miny: "3509000" + crs: EPSG:3034 + - bbox: + maxx: "3644850" + maxy: "4155860" + minx: "3016760" + miny: "3812640" + crs: EPSG:3035 + - bbox: + maxx: "820873" + maxy: "7503110" + minx: "281318" + miny: "6483220" + crs: EPSG:3857 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4258 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4326 + - bbox: + maxx: "7.37403" + maxy: "55.7212" + minx: "2.52713" + miny: "50.2129" + crs: CRS:84 + keywords: + - Aquaduct + - Brug + - Duikersifonhevel + - Stuw + - Vastedam + - AfvoerAanvoergebied + - Waterkering + - Sluis + - HydroObject + - HVD + - Aardobservatie en milieu + layers: + - abstract: Watercourse + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Waterbody.gpkg + columns: + - name: gml_id + - name: beginlifespanversion + - name: condition + - name: delineationknown + - name: endlifespanversion + - name: length + - name: level + - name: localid + - name: localtype + - name: name + - name: namespace + - name: origin + - name: persistence + - name: streamorder + - name: tidal + - name: widthrange + geometryType: MultiLineString + tableName: watercourse + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Hydroobject + - Waterbody + - Watercourse + - River + - Stream + - Lake + - Reservoir + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.Waterbodies + styles: + - name: HY.PhysicalWaters.Waterbodies.Default + title: Water bodies default style + visualization: watercourse.style + title: Waterbody + visible: true + - abstract: Drainage basin + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Catchment.gpkg + columns: + - name: gml_id + - name: area + - name: beginlifespanversion + - name: endlifespanversion + - name: localid + - name: name + - name: namespace + - name: order + - name: orderscheme + - name: origin + - name: outlet + - name: scope + geometryType: MultiPolygon + tableName: drainagebasin + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - AfvoergebiedAanvoergebied + - Catchment + - Basin + - Catchment Area + - Drainage basin + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.Catchments + styles: + - name: HY.PhysicalWaters.Catchments.Default + title: Drainage Basin default style + visualization: drainagebasin.style + title: Catchment + visible: true + - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, + lock. + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Man Made + - Duikersifonhevel + - Aquaduct + - Brug + - Stuw + - Vastedam + - Waterkering + - Sluis + - Bridge + - Aquaduct + - Dam + - Weir + - Lock + - Ford + - Dyke + layers: + - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, + lock. + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg + columns: + - name: gml_id + - name: beginlifespanversion + - name: condition + - name: endlifespanversion + - name: localid + - name: name + - name: namespace + geometryType: Point + tableName: lock + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Man Made + - Duikersifonhevel + - Aquaduct + - Brug + - Stuw + - Vastedam + - Waterkering + - Sluis. + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.ManMadeObject.lock + styles: + - name: HY.PhysicalWaters.ManMadeObject.Default + title: Lock + visualization: lock.style + title: Man-made Object + visible: false + - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, + lock. + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg + columns: + - name: gml_id + - name: beginlifespanversion + - name: condition + - name: endlifespanversion + - name: localid + - name: name + - name: namespace + - name: type + geometryType: Point + tableName: crossingpoint + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Man Made + - Duikersifonhevel + - Aquaduct + - Brug + - Stuw + - Vastedam + - Waterkering + - Sluis. + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.ManMadeObject.crossingpoint + styles: + - name: HY.PhysicalWaters.ManMadeObject.Default + title: Crossing Point + visualization: crossingpoint.style + title: Man-made Object + visible: false + - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, + lock. + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg + columns: + - name: gml_id + - name: beginlifespanversion + - name: condition + - name: endlifespanversion + - name: localid + - name: name + - name: namespace + geometryType: Point + tableName: sluice + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Man Made + - Duikersifonhevel + - Aquaduct + - Brug + - Stuw + - Vastedam + - Waterkering + - Sluis. + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.ManMadeObject.sluice + styles: + - name: HY.PhysicalWaters.ManMadeObject.Default + title: Sluice + visualization: sluice.style + title: Man-made Object + visible: false + - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, + lock. + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg + columns: + - name: gml_id + - name: beginlifespanversion + - name: condition + - name: endlifespanversion + - name: localid + - name: name + - name: namespace + geometryType: MultiLineString + tableName: embankment + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Man Made + - Duikersifonhevel + - Aquaduct + - Brug + - Stuw + - Vastedam + - Waterkering + - Sluis. + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.ManMadeObject.embankment + styles: + - name: HY.PhysicalWaters.ManMadeObject.Default + title: Embankment + visualization: embankment.style + title: Man-made Object + visible: false + - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, + lock. + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg + columns: + - name: gml_id + - name: beginlifespanversion + - name: condition + - name: endlifespanversion + - name: localid + - name: name + - name: namespace + - name: type + geometryType: MultiLineString + tableName: crossingline + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Man Made + - Duikersifonhevel + - Aquaduct + - Brug + - Stuw + - Vastedam + - Waterkering + - Sluis. + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.ManMadeObject.crossingline + styles: + - name: HY.PhysicalWaters.ManMadeObject.Default + title: Crossing Line + visualization: crossingline.style + title: Man-made Object + visible: false + - abstract: Crossing line, crossing point, dam or weir, sluice, embankment, + lock. + authority: + name: hwh + spatialDatasetIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + url: https://www.hetwaterschapshuis.nl/ + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/hwh/hydrografie/aaa/1/Man-madeObject.gpkg + columns: + - name: gml_id + - name: beginlifespanversion + - name: condition + - name: endlifespanversion + - name: localid + - name: name + - name: namespace + geometryType: Point + tableName: damorweir + datasetMetadataUrl: + csw: + metadataIdentifier: 07575774-57a1-4419-bab4-6c88fdeb02b2 + keywords: + - Man Made + - Duikersifonhevel + - Aquaduct + - Brug + - Stuw + - Vastedam + - Waterkering + - Sluis. + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.ManMadeObject.damorweir + styles: + - name: HY.PhysicalWaters.ManMadeObject.Default + title: Dam or Weir + visualization: damorweir.style + title: Man-made Object + visible: false + maxscaledenominator: "50000" + minscaledenominator: "1" + name: HY.PhysicalWaters.ManMadeObject + styles: + - name: HY.PhysicalWaters.ManMadeObject.Default + title: Man-made objects default style + title: Man-made Object + visible: true + title: Waterschappen Hydrografie (INSPIRE geharmoniseerd) WMS + visible: true + ownerInfoRef: pdok + prefix: hydrografie + stylingAssets: + blobKeys: + - resources/images/hwh/hydrografie/bridge.png + configMapRefs: + - keys: + - bridge.symbol + - x.symbol + - watercourse.style + - drainagebasin.style + - lock.style + - crossingpoint.style + - sluice.style + - embankment.style + - crossingline.style + - damorweir.style + name: includes + title: Waterschappen Hydrografie (INSPIRE geharmoniseerd) WMS + url: https://service.pdok.nl/hwh/hydrografie/wms/v2_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml new file mode 100644 index 0000000..8b53f3a --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/input/wms_group_and_toplayer.yaml @@ -0,0 +1,783 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + pdok.nl/wms-service-metadata-uuid: 97cf6a64-9cfc-4ce6-9741-2db44fd27fca + creationTimestamp: null + labels: + dataset: kadastralekaart + dataset-owner: kadaster + service-type: wms + service-version: v5_0 + name: kadaster-kadastralekaart +spec: + healthCheck: + mimetype: image/png + querystring: language=dut&SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=193882.0336615453998,470528.1693874415942,193922.4213813782844,470564.250484353397&CRS=EPSG:28992&WIDTH=769&HEIGHT=687&LAYERS=OpenbareRuimteNaam,Bebouwing,Perceel,KadastraleGrens&FORMAT=image/png&DPI=96&MAP_RESOLUTION=96&FORMAT_OPTIONS=dpi:96&TRANSPARENT=TRUE + options: + automaticCasing: true + disableWebserviceProxy: false + includeIngress: true + prefetchData: true + rewriteGroupToDataLayers: true + validateChildStyleNameEqual: false + validateRequests: true + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 104M + memory: 103M + requests: + cpu: "1001" + ephemeral-storage: 102M + memory: 101M + service: + abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. Fungeert + als schakel tussen terrein en registratie, vervult voor externe gebruiker vaak + een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker eigen + informatie kan vastleggen en presenteren. + accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + defResolution: 91 + keywords: + - Kadaster + - Kadastrale percelen + - Kadastrale grens + - Kadastrale kaart + - Bebouwing + - Nummeraanduidingreeks + - Openbare ruimte naam + - Perceel + - Grens + - Kwaliteit + - Kwaliteitslabels + - HVD + - Geospatiale data + layer: + abstract: Overzicht van de ligging van de kadastrale percelen in Nederland. + Fungeert als schakel tussen terrein en registratie, vervult voor externe gebruiker + vaak een referentiefunctie, een ondergrond ten opzichte waarvan de gebruiker + eigen informatie kan vastleggen en presenteren. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + - bbox: + maxx: "795163" + maxy: "6181970" + minx: "-470271" + miny: "5562310" + crs: EPSG:25831 + - bbox: + maxx: "397827" + maxy: "6190420" + minx: "62461.6" + miny: "5565550" + crs: EPSG:25832 + - bbox: + maxx: "3220070" + maxy: "3840030" + minx: "2613360" + miny: "3509000" + crs: EPSG:3034 + - bbox: + maxx: "3644850" + maxy: "4155860" + minx: "3016760" + miny: "3812640" + crs: EPSG:3035 + - bbox: + maxx: "820873" + maxy: "7503110" + minx: "281318" + miny: "6483220" + crs: EPSG:3857 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4258 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4326 + - bbox: + maxx: "7.37403" + maxy: "55.7212" + minx: "2.52713" + miny: "50.2129" + crs: CRS:84 + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Kadaster + - Kadastrale percelen + - Kadastrale grens + layers: + - abstract: De laag Bebouwing is een selectie op panden van de BGT. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Bebouwing + layers: + - abstract: De laag Bebouwing is een selectie op panden van de BGT. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/pand.gpkg + columns: + - name: object_begin_tijd + - alias: LV-publicatiedatum + name: lv_publicatiedatum + - name: relatieve_hoogteligging + - name: in_onderzoek + - name: tijdstip_registratie + - name: identificatie_namespace + - alias: identificatieLokaalID + name: identificatie_lokaal_id + - name: bronhouder + - alias: bgt-status + name: bgt_status + - alias: plus-status + name: plus_status + - alias: identificatieBAGPND + name: identificatie_bag_pnd + geometryType: Polygon + tableName: pand + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Bebouwing + maxscaledenominator: "6001" + minscaledenominator: "50" + name: Bebouwingvlak + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard + title: Standaardvisualisatie + visualization: bebouwing.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + visualization: bebouwing_kwaliteit.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + visualization: bebouwing_print.style + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:bebouwing + title: Standaardvisualisatie Bebouwing + visualization: bebouwing.group.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:bebouwing + title: Kwaliteitsvisualisatie Bebouwing + visualization: bebouwing_kwaliteit.group.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:bebouwing + title: Printvisualisatie Bebouwing + visualization: bebouwing_print.group.style + title: Bebouwingvlak + visible: true + - abstract: De laag Bebouwing is een selectie op panden van de BGT. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/pand_nummeraanduiding.gpkg + columns: + - alias: bebouwingID + name: bebouwing_id + - name: hoek + - name: tekst + - alias: identificatie_BAGVBOLaagsteHuisnummer + name: bag_vbo_laagste_huisnummer + - alias: identificatie_BAGVBOHoogsteHuisnummer + name: bag_vbo_hoogste_huisnummer + - name: hoek + geometryType: Point + tableName: pand_nummeraanduiding + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Nummeraanduidingreeks + maxscaledenominator: "2001" + minscaledenominator: "50" + name: Nummeraanduidingreeks + styles: + - abstract: Standaarvisualisatie van de nummeraanduidingreeks. + name: standaard + title: Standaardvisualisatie + visualization: nummeraanduidingreeks.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + visualization: nummeraanduidingreeks_kwaliteit.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + visualization: nummeraanduidingreeks_print.style + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:bebouwing + title: Standaardvisualisatie Bebouwing + visualization: nummeraanduidingreeks.group.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:bebouwing + title: Kwaliteitsvisualisatie Bebouwing + visualization: nummeraanduidingreeks_kwaliteit.group.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:bebouwing + title: Printvisualisatie Bebouwing + visualization: nummeraanduidingreeks_print.group.style + title: Nummeraanduidingreeks + visible: true + maxscaledenominator: "6001" + minscaledenominator: "50" + name: Bebouwing + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:bebouwing + title: Standaardvisualisatie Bebouwing + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:bebouwing + title: Kwaliteitsvisualisatie Bebouwing + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:bebouwing + title: Printvisualisatie Bebouwing + title: Bebouwing + visible: true + - abstract: De laag Openbareruimtenaam is een selectie op de openbare ruimte + labels van de BGT met een bgt-status "bestaand" die een classificatie (openbareruimtetype) + Weg en Water hebben. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/openbareruimtelabel.gpkg + columns: + - name: object_begin_tijd + - alias: LV-publicatiedatum + name: lv_publicatiedatum + - name: relatieve_hoogteligging + - name: in_onderzoek + - name: tijdstip_registratie + - name: identificatie_namespace + - alias: identificatieLokaalID + name: identificatie_lokaal_id + - name: bronhouder + - alias: bgt-status + name: bgt_status + - alias: plus-status + name: plus_status + - alias: identificatieBAGOPR + name: identificatie_bag_opr + - name: tekst + - name: hoek + - name: openbare_ruimte_type + geometryType: Point + tableName: openbareruimtelabel + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Openbare ruimte naam + maxscaledenominator: "2001" + minscaledenominator: "50" + name: OpenbareRuimteNaam + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard + title: Standaardvisualisatie + visualization: openbareruimtenaam.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + visualization: openbareruimtenaam_kwaliteit.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + visualization: openbareruimtenaam_print.style + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:openbareruimtenaam + title: Standaardvisualisatie OpenbareRuimteNaam + visualization: openbareruimtenaam.group.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:openbareruimtenaam + title: Kwaliteitsvisualisatie OpenbareRuimteNaam + visualization: openbareruimtenaam_kwaliteit.group.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:openbareruimtenaam + title: Printvisualisatie OpenbareRuimteNaam + visualization: openbareruimtenaam_print.group.style + title: OpenbareRuimteNaam + visible: true + - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen heeft + gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. Een + perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal + geรฏdentificeerd is en met kadastrale grenzen begrensd is. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Perceel + - Kadastrale percelen + layers: + - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen + heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. + Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal + geรฏdentificeerd is en met kadastrale grenzen begrensd is. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/perceel.gpkg + columns: + - name: identificatie_namespace + - alias: identificatieLokaalID + name: identificatie_lokaal_id + - name: begin_geldigheid + - name: tijdstip_registratie + - name: volgnummer + - name: status_historie_code + - name: status_historie_waarde + - name: kadastrale_gemeente_code + - name: kadastrale_gemeente_waarde + - name: sectie + - alias: AKRKadastraleGemeenteCodeCode + name: akr_kadastrale_gemeente_code_code + - alias: AKRKadastraleGemeenteCodeWaarde + name: akr_kadastrale_gemeente_code_waarde + - name: kadastrale_grootte_waarde + - name: soort_grootte_code + - name: soort_grootte_waarde + - name: perceelnummer + - name: perceelnummer_rotatie + - name: perceelnummer_verschuiving_delta_x + - name: perceelnummer_verschuiving_delta_y + - name: perceelnummer_plaatscoordinaat_x + - name: perceelnummer_plaatscoordinaat_y + geometryType: Polygon + tableName: perceel + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Kadastrale percelen + maxscaledenominator: "6001" + minscaledenominator: "50" + name: Perceelvlak + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard + title: Standaardvisualisatie + visualization: perceelvlak.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + visualization: perceelvlak_kwaliteit.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + visualization: perceelvlak_print.style + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:perceel + title: Standaardvisualisatie Perceel + visualization: perceelvlak.group.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + visualization: perceelvlak_kwaliteit.group.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:perceel + title: Printvisualisatie Perceel + visualization: perceelvlak_print.group.style + title: Perceelvlak + visible: true + - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen + heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. + Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal + geรฏdentificeerd is en met kadastrale grenzen begrensd is. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/perceel_label.gpkg + columns: + - alias: perceelID + name: perceel_id + - name: perceelnummer + - name: rotatie + - name: verschuiving_delta_x + - name: verschuiving_delta_y + geometryType: Point + tableName: perceel_label + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Kadastrale percelen + maxscaledenominator: "6001" + minscaledenominator: "50" + name: Label + styles: + - abstract: Standaarvisualisatie van het label. + name: standaard + title: Standaardvisualisatie + visualization: label.style + - abstract: Standaarvisualisatie van het label. + name: standaard:perceel + title: Standaardvisualisatie Perceel + visualization: label.group.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + visualization: label_kwaliteit.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + visualization: label_kwaliteit.group.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + visualization: label_print.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:perceel + title: Printvisualisatie Perceel + visualization: label_print.group.style + title: Label + visible: true + - abstract: Een perceel is een stuk grond waarvan het Kadaster de grenzen + heeft gemeten of gaat meten en dat bij het Kadaster een eigen nummer heeft. + Een perceel is een begrensd deel van het Nederlands grondgebied dat kadastraal + geรฏdentificeerd is en met kadastrale grenzen begrensd is. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/perceel_bijpijling.gpkg + columns: + - alias: perceelID + name: perceel_id + geometryType: LineString + tableName: perceel_bijpijling + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Kadastrale percelen + maxscaledenominator: "6001" + minscaledenominator: "50" + name: Bijpijling + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard + title: Standaardvisualisatie + visualization: bijpijling.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + visualization: bijpijling_kwaliteit.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + visualization: bijpijling_print.style + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:perceel + title: Standaardvisualisatie Perceel + visualization: bijpijling.group.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + visualization: bijpijling_kwaliteit.group.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:perceel + title: Printvisualisatie Perceel + visualization: bijpijling_print.group.style + title: Bijpijling + visible: true + maxscaledenominator: "6001" + minscaledenominator: "50" + name: Perceel + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:perceel + title: Standaardvisualisatie Perceel + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:perceel + title: Kwaliteitsvisualisatie Perceel + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:perceel + title: Printvisualisatie Perceel + title: Perceel + visible: true + - abstract: Een Kadastrale Grens is de weergave van een grens op de kadastrale + kaart die door de dienst van het Kadaster tussen percelen (voorlopig) vastgesteld + wordt, op basis van inlichtingen van belanghebbenden en met gebruikmaking + van de aan de kadastrale kaart ten grondslag liggende bescheiden die in + elk geval de landmeetkundige gegevens bevatten van hetgeen op die kaart + wordt weergegeven. + authority: + name: kadaster + spatialDatasetIdentifier: 06b6c650-cdb1-11dd-ad8b-0800200c9a64 + url: https://www.kadaster.nl + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/kadaster/kadastralekaart_brk/aaa/1/kadastrale_grens.gpkg + columns: + - name: begin_geldigheid + - name: tijdstip_registratie + - name: volgnummer + - name: status_historie_code + - name: status_historie_waarde + - name: identificatie_namespace + - alias: identificatieLokaalID + name: identificatie_lokaal_id + - name: type_grens_code + - name: type_grens_waarde + - alias: ClassificatieKwaliteitCode + name: classificatie_kwaliteit_code + - alias: ClassificatieKwaliteitWaarde + name: classificatie_kwaliteit_waarde + - name: perceel_links_identificatie_namespace + - alias: perceelLinksIdentificatieLokaalID + name: perceel_links_identificatie_lokaal_id + - name: perceel_rechts_identificatie_namespace + - alias: perceelRechtsIdentificatieLokaalID + name: perceel_rechts_identificatie_lokaal_id + geometryType: LineString + tableName: kadastrale_grens + datasetMetadataUrl: + csw: + metadataIdentifier: a29917b9-3426-4041-a11b-69bcb2256904 + keywords: + - Grens + - Kadastrale grenzen + maxscaledenominator: "6001" + minscaledenominator: "50" + name: KadastraleGrens + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard + title: Standaardvisualisatie + visualization: kadastralegrens.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + visualization: kadastralegrens_kwaliteit.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + visualization: kadastralegrens_print.style + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard:kadastralegrens + title: Standaardvisualisatie KadastraleGrens + visualization: kadastralegrens.group.style + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit:kadastralegrens + title: Kwaliteitsvisualisatie KadastraleGrens + visualization: kadastralegrens_kwaliteit.group.style + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print:kadastralegrens + title: Printvisualisatie KadastraleGrens + visualization: kadastralegrens_print.group.style + title: KadastraleGrens + visible: true + maxscaledenominator: "6001" + name: Kadastralekaart + styles: + - abstract: Standaardvisualisatie met grenzen op basis van type (definitief, + voorlopig of administratief). + name: standaard + title: Standaardvisualisatie + - abstract: Kwaliteitsvisualisatie met grenzen op basis van kwaliteitsklasse + (B, C, D of E). + name: kwaliteit + title: Kwaliteitsvisualisatie + - abstract: Visualisatie ten behoeve van afdrukken op 180 dpi. + name: print + title: Printvisualisatie + title: KadastraleKaartv5 + visible: true + maxSize: 10000 + ownerInfoRef: pdok + prefix: kadastralekaart + resolution: 91 + stylingAssets: + blobKeys: + - resources/fonts/liberation-sans.ttf + - resources/fonts/liberation-sans-italic.ttf + configMapRefs: + - keys: + - bebouwing.style + - bebouwing_kwaliteit.style + - bebouwing_print.style + - bebouwing.group.style + - bebouwing_kwaliteit.group.style + - bebouwing_print.group.style + - nummeraanduidingreeks.style + - nummeraanduidingreeks_kwaliteit.style + - nummeraanduidingreeks_print.style + - nummeraanduidingreeks.group.style + - nummeraanduidingreeks_kwaliteit.group.style + - nummeraanduidingreeks_print.group.style + - openbareruimtenaam.style + - openbareruimtenaam_kwaliteit.style + - openbareruimtenaam_print.style + - openbareruimtenaam.group.style + - openbareruimtenaam_kwaliteit.group.style + - openbareruimtenaam_print.group.style + - perceelvlak.style + - perceelvlak_kwaliteit.style + - perceelvlak_print.style + - perceelvlak.group.style + - perceelvlak_kwaliteit.group.style + - perceelvlak_print.group.style + - label.style + - label.group.style + - label_kwaliteit.style + - label_kwaliteit.group.style + - label_print.style + - label_print.group.style + - bijpijling.style + - bijpijling_kwaliteit.style + - bijpijling_print.style + - bijpijling.group.style + - bijpijling_kwaliteit.group.style + - bijpijling_print.group.style + - kadastralegrens.style + - kadastralegrens_kwaliteit.style + - kadastralegrens_print.style + - kadastralegrens.group.style + - kadastralegrens_kwaliteit.group.style + - kadastralegrens_print.group.style + name: includes + title: Kadastrale Kaart (WMS) + url: https://service.pdok.nl/kadaster/kadastralekaart/wms/v5_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml new file mode 100644 index 0000000..845272e --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/input/wms_groupless.yaml @@ -0,0 +1,298 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + lifecycle-phase: prod + service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb + creationTimestamp: null + labels: + dataset: nwbwegen + dataset-owner: rws + service-type: wms + service-version: v1_0 + name: rws-nwbwegen-v1-0 +spec: + healthCheck: + boundingbox: + maxx: "135416.03" + maxy: "457187.82" + minx: "135134.89" + miny: "457152.55" + options: + automaticCasing: true + disableWebserviceProxy: false + includeIngress: true + prefetchData: true + rewriteGroupToDataLayers: false + validateChildStyleNameEqual: false + validateRequests: true + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 1535Mi + memory: 4G + requests: + cpu: "2" + ephemeral-storage: 1535Mi + memory: 4G + service: + abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - wegen. + Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal Wegen + Bestand - Wegen is een digitaal geografisch bestand van alle wegen in Nederland. + Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als het Rijk, + provincies, gemeenten en waterschappen, echter alleen voor zover deze zijn voorzien + van een straatnaam of nummer. + accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + inspire: + language: dut + serviceMetadataUrl: + csw: + metadataIdentifier: f2437a92-ddd3-4777-a1bc-fdf4b4a7fcb8 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + - Hectometerpunten + - HVD + - Mobiliteit + layer: + abstract: Dit is de web map service van het Nationaal Wegen Bestand (NWB) - + wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal + Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in + Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders + als het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover + deze zijn voorzien van een straatnaam of nummer. + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + - bbox: + maxx: "795163" + maxy: "6181970" + minx: "-470271" + miny: "5562310" + crs: EPSG:25831 + - bbox: + maxx: "397827" + maxy: "6190420" + minx: "62461.6" + miny: "5565550" + crs: EPSG:25832 + - bbox: + maxx: "3220070" + maxy: "3840030" + minx: "2613360" + miny: "3509000" + crs: EPSG:3034 + - bbox: + maxx: "3644850" + maxy: "4155860" + minx: "3016760" + miny: "3812640" + crs: EPSG:3035 + - bbox: + maxx: "820873" + maxy: "7503110" + minx: "281318" + miny: "6483220" + crs: EPSG:3857 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4258 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4326 + - bbox: + maxx: "7.37403" + maxy: "55.7212" + minx: "2.52713" + miny: "50.2129" + crs: CRS:84 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + - Hectometerpunten + - HVD + - Mobiliteit + layers: + - abstract: Deze laag bevat de wegvakken uit het Nationaal Wegen bestand (NWB) + en geeft gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, + routenummer, wegbeheerder, huisnummers, enz. weer. + authority: + name: rws + spatialDatasetIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + url: https://www.rijkswaterstaat.nl + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg + columns: + - name: objectid + - name: wvk_id + - name: wvk_begdat + - name: jte_id_beg + - name: jte_id_end + - name: wegbehsrt + - name: wegnummer + - name: wegdeelltr + - name: hecto_lttr + - name: bst_code + - name: rpe_code + - name: admrichtng + - name: rijrichtng + - name: stt_naam + - name: stt_bron + - name: wpsnaam + - name: gme_id + - name: gme_naam + - name: hnrstrlnks + - name: hnrstrrhts + - name: e_hnr_lnks + - name: e_hnr_rhts + - name: l_hnr_lnks + - name: l_hnr_rhts + - name: begafstand + - name: endafstand + - name: beginkm + - name: eindkm + - name: pos_tv_wol + - name: wegbehcode + - name: wegbehnaam + - name: distrcode + - name: distrnaam + - name: dienstcode + - name: dienstnaam + - name: wegtype + - name: wgtype_oms + - name: routeltr + - name: routenr + - name: routeltr2 + - name: routenr2 + - name: routeltr3 + - name: routenr3 + - name: routeltr4 + - name: routenr4 + - name: wegnr_aw + - name: wegnr_hmp + - name: geobron_id + - name: geobron_nm + - name: bronjaar + - name: openlr + - name: bag_orl + - name: frc + - name: fow + - name: alt_naam + - name: alt_nr + - name: rel_hoogte + - name: st_lengthshape + geometryType: MultiLineString + tableName: wegvakken + datasetMetadataUrl: + csw: + metadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + maxscaledenominator: "50000" + minscaledenominator: "1" + name: wegvakken + styles: + - name: wegvakken + title: NWB - Wegvakken + visualization: wegvakken.style + title: Wegvakken + visible: true + - abstract: Deze laag bevat de hectopunten uit het Nationaal Wegen Bestand (NWB) + en geeft gedetailleerde informatie per hectopunt zoals hectometrering, afstand, + zijde en hectoletter weer. + authority: + name: rws + spatialDatasetIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + url: https://www.rijkswaterstaat.nl + boundingBoxes: + - bbox: + maxx: "308126.88473339565" + maxy: "858328.516489961" + minx: "-59188.44333693248" + miny: "304984.64144318487" + crs: EPSG:28992 + data: + gpkg: + blobKey: geopackages/rws/nwbwegen/410a6d1e-e767-41b4-ba8d-9e1e955dd013/1/nwb_wegen.gpkg + columns: + - name: objectid + - name: hectomtrng + - name: afstand + - name: wvk_id + - name: wvk_begdat + - name: zijde + - name: hecto_lttr + geometryType: MultiPoint + tableName: hectopunten + datasetMetadataUrl: + csw: + metadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Hectometerpunten + maxscaledenominator: "50000" + minscaledenominator: "1" + name: hectopunten + styles: + - name: hectopunten + title: NWB - Hectopunten + visualization: hectopunten.style + title: Hectopunten + visible: true + title: NWB - Wegen WMS + visible: true + ownerInfoRef: pdok + prefix: nwbwegen + stylingAssets: + blobKeys: + - resources/fonts/liberation-sans.ttf + configMapRefs: + - keys: + - nwb_wegen_hectopunten.symbol + - hectopunten.style + - wegvakken.style + name: includes + title: NWB - Wegen WMS + url: https://service.pdok.nl/rws/nwbwegen/wms/v1_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml new file mode 100644 index 0000000..8735da6 --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/input/wms_postgis.yaml @@ -0,0 +1,185 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + pdok.nl/wms-service-metadata-uuid: fa069f74-9837-4d63-b2ac-b337b5de86b1 + creationTimestamp: null + labels: + dataset: terugmeldingen + dataset-owner: brt + service-type: wms + service-version: v1_0 + name: v1-0 +spec: + options: + automaticCasing: true + disableWebserviceProxy: false + includeIngress: true + prefetchData: true + rewriteGroupToDataLayers: false + validateChildStyleNameEqual: false + validateRequests: true + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 20Mi + service: + abstract: De BRT terugmeldingenservice bevat alle recente meldingen op BRT objecten + waar twijfel over de juistheid bestaat. Zowel terugmeldingen op de TOP10 als + meldingen die gemaakt zijn op de gegeneraliseerde kaartproducten (TOP25, TOP50, + TOP100, TOP250) worden hierin geregistreerd. Daarnaast kan je de inhoud en status + van de meldingen inzien. Ook een vermoedelijke fout geconstateerd? Doe een melding + op https://verbeterdekaart.kadaster.nl + accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + keywords: + - Basisregistratie Topografie + - BRT + - terugmeldingen + - TOP10NL + - TOP25 + - TOP50 + - TOP100 + - TOP250 + - in onderzoek register + - verbeter de kaart + - verbeterdekaart + layer: + abstract: De BRT terugmeldingenservice bevat alle recente meldingen op BRT objecten + waar twijfel over de juistheid bestaat. Zowel terugmeldingen op de TOP10 als + meldingen die gemaakt zijn op de gegeneraliseerde kaartproducten (TOP25, TOP50, + TOP100, TOP250) worden hierin geregistreerd. Daarnaast kan je de inhoud en + status van de meldingen inzien. Ook een vermoedelijke fout geconstateerd? + Doe een melding op https://verbeterdekaart.kadaster.nl + boundingBoxes: + - bbox: + maxx: "300000" + maxy: "629000" + minx: "-7000" + miny: "289000" + crs: EPSG:28992 + - bbox: + maxx: "795163" + maxy: "6181970" + minx: "-470271" + miny: "5562310" + crs: EPSG:25831 + - bbox: + maxx: "397827" + maxy: "6190420" + minx: "62461.6" + miny: "5565550" + crs: EPSG:25832 + - bbox: + maxx: "3220070" + maxy: "3840030" + minx: "2613360" + miny: "3509000" + crs: EPSG:3034 + - bbox: + maxx: "3644850" + maxy: "4155860" + minx: "3016760" + miny: "3812640" + crs: EPSG:3035 + - bbox: + maxx: "820873" + maxy: "7503110" + minx: "281318" + miny: "6483220" + crs: EPSG:3857 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4258 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4326 + - bbox: + maxx: "7.37403" + maxy: "55.7212" + minx: "2.52713" + miny: "50.2129" + crs: CRS:84 + keywords: + - Basisregistratie Topografie + - BRT + - terugmeldingen + - TOP10NL + - TOP25 + - TOP50 + - TOP100 + - TOP250 + - in onderzoek register + - verbeter de kaart + - verbeterdekaart + layers: + - abstract: Alle recente BRT terugmeldingen gedaan door BRT gebruikers. + authority: + name: brt + spatialDatasetIdentifier: 07c7d650-cdb1-11dd-ad8b-0800200c9a60 + url: http://www.brt.nl + boundingBoxes: + - bbox: + maxx: "300000" + maxy: "629000" + minx: "-7000" + miny: "289000" + crs: EPSG:28992 + data: + postgis: + columns: + - name: meldingsnummer_volledig + - name: tijdstip_registratie + - name: status + - name: omschrijving + - name: bronhoudercode + - name: bronhoudernaam + - name: tijdstip_statuswijziging + - name: toelichting + - name: objectid + - name: objecttype + - name: hoogte_vanaf_maaiveld + geometryType: Point + tableName: brtterugmeldingen.brtterugmeldingen_v1 + datasetMetadataUrl: + csw: + metadataIdentifier: 7a84c4de-4ec0-4202-a8d0-792fb7d39d1f + keywords: + - brtterugmeldingen + name: brtterugmeldingen + styles: + - legend: + blobKey: ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/terugmeldingen-legend.png + name: brtterugmeldingen:terugmeldingen + title: Terugmeldingen + visualization: terugmeldingen.style + title: BRT Terugmeldingen + visible: true + title: BRT Terugmeldingen WMS + visible: true + ownerInfoRef: pdok + prefix: terugmeldingen + stylingAssets: + blobKeys: + - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/afgerond-blauw.png + - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/afgewezen-rood.png + - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/doorgestuurd-grijs.png + - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/geparkeerd-kobaltblauw.png + - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/goedgekeurd-groen.png + - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/nieuw-geel.png + - ${BLOBS_RESOURCES_BUCKET}/images/terugmeldingen/onderzoek-oranje.png + configMapRefs: + - keys: + - terugmeldingen.symbol + - terugmeldingen.style + name: includes + title: BRT Terugmeldingen WMS + url: https://service.pdok.nl/brt/terugmeldingen/wms/v1_0 diff --git a/internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml b/internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml new file mode 100644 index 0000000..335dd6d --- /dev/null +++ b/internal/controller/mapfilegenerator/test_data/input/wms_tif.yaml @@ -0,0 +1,438 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + pdok.nl/wms-service-metadata-uuid: 70562932-e7dc-4ba2-ba4f-05863d02587c + creationTimestamp: null + labels: + dataset: luchtfotolabels + dataset-owner: bzk + service-type: wms + service-version: v1_0 + name: bzk-luchtfotolabels-v1-0 +spec: + healthCheck: + boundingbox: + maxx: "135531.2729437439411" + maxy: "457377.1306112145539" + minx: "135036.1077132325445" + miny: "456913.9317436855054" + horizontalPodAutoscalerPatch: + maxReplicas: 2 + minReplicas: 1 + options: + automaticCasing: false + disableWebserviceProxy: false + includeIngress: false + prefetchData: true + rewriteGroupToDataLayers: false + validateChildStyleNameEqual: false + validateRequests: false + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 6G + memory: 4G + requests: + cpu: "1" + ephemeral-storage: 6G + memory: 4G + service: + abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden + gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + accessConstraints: https://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + keywords: + - bzk + - luchtfotolabels + layer: + abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen worden + gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + - bbox: + maxx: "795163" + maxy: "6181970" + minx: "-470271" + miny: "5562310" + crs: EPSG:25831 + - bbox: + maxx: "397827" + maxy: "6190420" + minx: "62461.6" + miny: "5565550" + crs: EPSG:25832 + - bbox: + maxx: "3220070" + maxy: "3840030" + minx: "2613360" + miny: "3509000" + crs: EPSG:3034 + - bbox: + maxx: "3644850" + maxy: "4155860" + minx: "3016760" + miny: "3812640" + crs: EPSG:3035 + - bbox: + maxx: "820873" + maxy: "7503110" + minx: "281318" + miny: "6483220" + crs: EPSG:3857 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4258 + - bbox: + maxx: "55.7212" + maxy: "7.37403" + minx: "50.2129" + miny: "2.52713" + crs: EPSG:4326 + - bbox: + maxx: "7.37403" + maxy: "55.7212" + minx: "2.52713" + miny: "50.2129" + crs: CRS:84 + keywords: + - bzk + - luchtfotolabels + layers: + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + layers: + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/100pixkm_luforoads/100pixkm_luforoads.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "48001" + minscaledenominator: "24001" + name: luchtfotoroads_100pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: roads.style + title: Luchtfoto roads 100pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/200pixkm_luforoads/200pixkm_luforoads.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "2.0" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "24001" + minscaledenominator: "12001" + name: luchtfotoroads_200pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: roads.style + title: Luchtfoto roads 200pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/400pixkm_luforoads/400pixkm_luforoads.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "2.5" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "12001" + minscaledenominator: "6001" + name: luchtfotoroads_400pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: roads.style + title: Luchtfoto roads 400pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/800pixkm_luforoads/800pixkm_luforoads.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "6001" + minscaledenominator: "3001" + name: luchtfotoroads_800pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: roads.style + title: Luchtfoto roads 800pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/1600pixkm_luforoads/1600pixkm_luforoads.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "3001" + minscaledenominator: "1501" + name: luchtfotoroads_1600pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: roads.style + title: Luchtfoto roads 1600pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/100pixkm_lufolabels/100pixkm_lufolabels.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "48001" + minscaledenominator: "24001" + name: luchtfotolabels_100pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: labels.style + title: Luchtfoto labels 100pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/200pixkm_lufolabels/200pixkm_lufolabels.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "24001" + minscaledenominator: "12001" + name: luchtfotolabels_200pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: labels.style + title: Luchtfoto labels 200pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/400pixkm_lufolabels/400pixkm_lufolabels.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "12001" + minscaledenominator: "6001" + name: luchtfotolabels_400pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: labels.style + title: Luchtfoto labels 400pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/800pixkm_lufolabels/800pixkm_lufolabels.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "6001" + minscaledenominator: "3001" + name: luchtfotolabels_800pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: labels.style + title: Luchtfoto labels 800pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/1600pixkm_lufolabels/1600pixkm_lufolabels.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "3001" + minscaledenominator: "1501" + name: luchtfotolabels_1600pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: labels.style + title: Luchtfoto labels 1600pixkm + visible: true + - abstract: De luchtfoto labels bestaan uit weglabels en wegassen en kunnen + worden gebruikt als laag (overlay) op onder andere de PDOK luchtfoto. + authority: + name: kadaster + spatialDatasetIdentifier: 901647c2-802d-11e6-ae22-56b6b6499611 + url: http://www.kadaster.nl + data: + tif: + blobKey: tifs/bzk/luchtfotolabels/${GPKG_VERSION}/3200pixkm_lufolabels/3200pixkm_lufolabels.vrt + offsite: '#978E97' + resample: BILINEAR + oversampleRatio: "1" + datasetMetadataUrl: + csw: + metadataIdentifier: 6ca22f53-b072-42f4-b920-104c7c83cd28 + keywords: + - bzk + - luchtfotolabels + maxscaledenominator: "1501" + name: luchtfotolabels_3200pixkm + styles: + - name: luchtfotolabels + title: Luchtfotolabels + visualization: labels.style + title: Luchtfoto labels 3200pixkm + visible: true + name: lufolabels + styles: + - name: luchtfotolabels + title: Luchtfotolabels + title: Luchtfoto labels + visible: true + title: Luchtfoto Labels WMS + visible: true + ownerInfoRef: pdok + prefix: luchtfotolabels + stylingAssets: + configMapRefs: + - keys: + - roads.style + - labels.style + name: ${INCLUDES} + title: Luchtfoto Labels WMS + url: https://service.pdok.nl/bzk/luchtfotolabels/wms/v1_0 diff --git a/internal/controller/mapfilegenerator/types.go b/internal/controller/mapfilegenerator/types.go new file mode 100644 index 0000000..affd32d --- /dev/null +++ b/internal/controller/mapfilegenerator/types.go @@ -0,0 +1,136 @@ +package mapfilegenerator + +import ( + "path" + "regexp" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" +) + +//nolint:tagliatelle +type BaseServiceInput struct { + Title string `json:"service_title"` + Abstract string `json:"service_abstract"` + Keywords string `json:"service_keywords"` + Extent string `json:"service_extent"` + NamespacePrefix string `json:"service_namespace_prefix"` + NamespaceURI string `json:"service_namespace_uri"` + OnlineResource string `json:"service_onlineresource"` + Path string `json:"service_path"` + MetadataID string `json:"service_metadata_id"` + DatasetOwner *string `json:"dataset_owner,omitempty"` + AuthorityURL *string `json:"authority_url,omitempty"` + AutomaticCasing bool `json:"automatic_casing"` + DataEPSG string `json:"data_epsg"` + EPSGList []string `json:"epsg_list"` + DebugLevel int `json:"service_debug_level,omitempty"` + AccessConstraints string `json:"service_accessconstraints"` +} + +//nolint:tagliatelle +type WFSInput struct { + BaseServiceInput + MaxFeatures string `json:"service_wfs_maxfeatures"` + Layers []WFSLayer `json:"layers"` +} + +//nolint:tagliatelle +type WMSInput struct { + BaseServiceInput + Layers []WMSLayer `json:"layers"` + GroupLayers []GroupLayer `json:"group_layers"` + Symbols []string `json:"symbols"` + Fonts *string `json:"fonts,omitempty"` + Templates string `json:"templates,omitempty"` + OutputFormatJpg string `json:"outputformat_jpg"` + OutputFormatPng string `json:"outputformat_png8"` + MaxSize string `json:"maxSize"` + TopLevelName string `json:"top_level_name,omitempty"` + Resolution string `json:"resolution,omitempty"` + DefResolution string `json:"defresolution,omitempty"` +} + +//nolint:tagliatelle +type BaseLayer struct { + Name string `json:"name"` + Title string `json:"title"` + Abstract string `json:"abstract"` + Keywords string `json:"keywords"` + Extent string `json:"layer_extent"` + MetadataID string `json:"dataset_metadata_id"` + Columns []Column `json:"columns,omitempty"` + GeometryType *string `json:"geometry_type,omitempty"` + GeopackagePath *string `json:"gpkg_path,omitempty"` + TableName *string `json:"tablename,omitempty"` + Postgis *bool `json:"postgis,omitempty"` + MinScale *string `json:"minscale,omitempty"` + MaxScale *string `json:"maxscale,omitempty"` + TifPath *string `json:"tif_path,omitempty"` + Resample *string `json:"resample,omitempty"` + OversampleRatio *string `json:"oversample_ratio,omitempty"` + LabelNoClip bool `json:"label_no_clip,omitempty"` +} + +type WFSLayer struct { + BaseLayer +} + +//nolint:tagliatelle +type GroupLayer struct { + Name string `json:"name"` + Title string `json:"title"` + Abstract string `json:"abstract"` + StyleName string `json:"style_name"` + StyleTitle string `json:"style_title"` +} + +//nolint:tagliatelle +type WMSLayer struct { + BaseLayer + GroupName string `json:"group_name,omitempty"` + Styles []Style `json:"styles"` + Offsite string `json:"offsite,omitempty"` + GetFeatureInfoIncludesClass *bool `json:"get_feature_info_includes_class,omitempty"` +} + +type Column struct { + Name string `json:"name"` + Alias *string `json:"alias,omitempty"` +} + +type Style struct { + Path string `json:"path"` + Title string `json:"title,omitempty"` +} + +func SetDataFields[O pdoknlv3.WMSWFS](obj O, wmsLayer *WMSLayer, data pdoknlv3.Data) { + switch { + case data.Gpkg != nil: + gpkg := data.Gpkg + + wmsLayer.GeometryType = &gpkg.GeometryType + geopackageConstructedPath := "/srv/data/gpkg/" + path.Base(gpkg.BlobKey) + if !obj.Options().PrefetchData { + reReplace := regexp.MustCompile(`$[a-zA-Z0-9_]*]/`) + geopackageConstructedPath = path.Join("/vsiaz/geopackages", reReplace.ReplaceAllString(gpkg.BlobKey, "")) + } + wmsLayer.GeopackagePath = &geopackageConstructedPath + case data.TIF != nil: + tif := data.TIF + wmsLayer.GeometryType = smoothoperatorutils.Pointer("Raster") + wmsLayer.BaseLayer.TifPath = smoothoperatorutils.Pointer(path.Join(tifPath, path.Base(tif.BlobKey))) + if !obj.Options().PrefetchData { + reReplace := regexp.MustCompile(`$[a-zA-Z0-9_]*]/`) + wmsLayer.BaseLayer.TifPath = smoothoperatorutils.Pointer(path.Join("/vsiaz", reReplace.ReplaceAllString(tif.BlobKey, ""))) + } + wmsLayer.BaseLayer.Resample = &tif.Resample + wmsLayer.BaseLayer.OversampleRatio = &tif.OversampleRatio + wmsLayer.Offsite = smoothoperatorutils.PointerVal(tif.Offsite, "") + wmsLayer.GetFeatureInfoIncludesClass = &tif.GetFeatureInfoIncludesClass + case data.Postgis != nil: + postgis := data.Postgis + wmsLayer.Postgis = smoothoperatorutils.Pointer(true) + wmsLayer.GeometryType = &postgis.GeometryType + } +} diff --git a/internal/controller/mapperutils/utils.go b/internal/controller/mapperutils/utils.go new file mode 100644 index 0000000..675b651 --- /dev/null +++ b/internal/controller/mapperutils/utils.go @@ -0,0 +1,72 @@ +package mapperutils + +import ( + "strings" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + corev1 "k8s.io/api/core/v1" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func GetContainerResourceRequest[O pdoknlv3.WMSWFS](obj O, containerName string, resource corev1.ResourceName) *resource.Quantity { + for _, container := range obj.PodSpecPatch().Containers { + if container.Name == containerName { + q := container.Resources.Requests[resource] + if !q.IsZero() { + return &q + } + } + } + + return nil +} + +func GetContainerResourceLimit[O pdoknlv3.WMSWFS](obj O, containerName string, resource corev1.ResourceName) *resource.Quantity { + for _, container := range obj.PodSpecPatch().Containers { + if container.Name == containerName { + q := container.Resources.Limits[resource] + if !q.IsZero() { + return &q + } + } + } + + return nil +} + +// Use ephemeral volume when ephemeral storage is greater then 10Gi +func UseEphemeralVolume[O pdoknlv3.WMSWFS](obj O) (bool, *resource.Quantity) { + value := EphemeralStorageLimit(obj) + threshold := resource.MustParse("10Gi") + + if value != nil { + return value.Value() > threshold.Value(), value + } + + return false, nil +} + +func EphemeralStorageLimit[O pdoknlv3.WMSWFS](obj O) *resource.Quantity { + return GetContainerResourceLimit(obj, constants.MapserverName, corev1.ResourceEphemeralStorage) +} + +func EphemeralStorageRequest[O pdoknlv3.WMSWFS](obj O) *resource.Quantity { + return GetContainerResourceRequest(obj, constants.MapserverName, corev1.ResourceEphemeralStorage) +} + +func GetNamespaceURI(prefix string, ownerInfo *smoothoperatorv1.OwnerInfo) string { + return strings.ReplaceAll(*ownerInfo.Spec.NamespaceTemplate, "{{prefix}}", prefix) +} + +func AnyMatch[S ~[]E, E any](slice S, eql func(E) bool) bool { + for _, elem := range slice { + if eql(elem) { + return true + } + } + return false +} diff --git a/internal/controller/mapserver/deployment.go b/internal/controller/mapserver/deployment.go new file mode 100644 index 0000000..b8f4d30 --- /dev/null +++ b/internal/controller/mapserver/deployment.go @@ -0,0 +1,200 @@ +package mapserver + +import ( + "errors" + "strings" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + "github.com/pdok/mapserver-operator/internal/controller/utils" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/static" + "github.com/pdok/mapserver-operator/internal/controller/types" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +const mimeTextXML = "text/xml" + +func GetMapserverContainer[O pdoknlv3.WMSWFS](obj O, images types.Images) (*corev1.Container, error) { + livenessProbe, readinessProbe, startupProbe, err := getProbes(obj) + if err != nil { + return nil, err + } + + container := corev1.Container{ + Name: constants.MapserverName, + Image: images.MapserverImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{{ContainerPort: constants.MapserverPortNr, Protocol: corev1.ProtocolTCP}}, + Env: []corev1.EnvVar{ + { + Name: "SERVICE_TYPE", + Value: string(obj.Type()), + }, + { + Name: "MAPSERVER_CONFIG_FILE", + Value: "/srv/mapserver/config/default_mapserver.conf", + }, + GetMapfileEnvVar(obj), + }, + VolumeMounts: getVolumeMounts(obj.Mapfile() != nil), + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("800M"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("0.15"), + }, + }, + Lifecycle: &corev1.Lifecycle{PreStop: &corev1.LifecycleHandler{Exec: &corev1.ExecAction{Command: []string{"sleep", "15"}}}}, + StartupProbe: startupProbe, + ReadinessProbe: readinessProbe, + LivenessProbe: livenessProbe, + } + + if obj.Type() == pdoknlv3.ServiceTypeWMS && !obj.Options().DisableWebserviceProxy { + container.Resources.Requests[corev1.ResourceCPU] = resource.MustParse("0.1") + } + + return &container, nil +} + +func getVolumeMounts(customMapfile bool) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{ + utils.GetBaseVolumeMount(), + utils.GetDataVolumeMount(), + } + + staticFiles, _ := static.GetStaticFiles() + for _, name := range staticFiles { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: constants.MapserverName, + MountPath: "/srv/mapserver/config/" + name, + SubPath: name, + }) + } + if customMapfile { + volumeMounts = append(volumeMounts, utils.GetMapfileVolumeMount()) + } + + return volumeMounts +} + +func GetMapfileEnvVar[O pdoknlv3.WMSWFS](obj O) corev1.EnvVar { + mapFileName := "service.map" + if obj.Mapfile() != nil { + mapFileName = obj.Mapfile().ConfigMapKeyRef.Key + } + + return corev1.EnvVar{ + Name: "MS_MAPFILE", + Value: "/srv/data/config/mapfile/" + mapFileName, + } +} + +func getProbes[O pdoknlv3.WMSWFS](obj O) (livenessProbe *corev1.Probe, readinessProbe *corev1.Probe, startupProbe *corev1.Probe, err error) { + livenessProbe = getLivenessProbe(obj) + switch obj.Type() { + case pdoknlv3.ServiceTypeWFS: + wfs, _ := any(obj).(*pdoknlv3.WFS) + readinessProbe, err = getReadinessProbeForWFS(wfs) + if err != nil { + return nil, nil, nil, err + } + startupProbe, err = getStartupProbeForWFS(wfs) + if err != nil { + return nil, nil, nil, err + } + case pdoknlv3.ServiceTypeWMS: + wms, _ := any(obj).(*pdoknlv3.WMS) + readinessProbe, err = getReadinessProbeForWMS(wms) + if err != nil { + return nil, nil, nil, err + } + startupProbe, err = getStartupProbeForWMS(wms) + if err != nil { + return nil, nil, nil, err + } + } + return +} + +func getLivenessProbe[O pdoknlv3.WMSWFS](obj O) *corev1.Probe { + queryString := "SERVICE=" + string(obj.Type()) + "&request=GetCapabilities" + return getProbe(queryString, mimeTextXML) +} + +func getReadinessProbeForWFS(wfs *pdoknlv3.WFS) (*corev1.Probe, error) { + queryString, mime, err := wfs.ReadinessQueryString() + if err != nil { + return nil, err + } + return getProbe(queryString, mime), nil +} + +func getReadinessProbeForWMS(wms *pdoknlv3.WMS) (*corev1.Probe, error) { + queryString, mime, err := wms.ReadinessQueryString() + if err != nil { + return nil, err + } + + return getProbe(queryString, mime), nil +} + +func getStartupProbeForWFS(wfs *pdoknlv3.WFS) (*corev1.Probe, error) { + if hc := wfs.Spec.HealthCheck; hc != nil { + return getProbe(hc.Querystring, hc.Mimetype), nil + } + + var typeNames []string + for _, ft := range wfs.Spec.Service.FeatureTypes { + typeNames = append(typeNames, ft.Name) + } + if len(typeNames) == 0 { + return nil, errors.New("cannot get startup probe for WFS, featuretypes could not be found") + } + + queryString := "SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=" + strings.Join(typeNames, ",") + "&STARTINDEX=0&COUNT=1" + return getProbe(queryString, mimeTextXML), nil +} + +func getStartupProbeForWMS(wms *pdoknlv3.WMS) (*corev1.Probe, error) { + if hc := wms.Spec.HealthCheck; hc != nil && hc.Querystring != nil { + return getProbe(*hc.Querystring, *hc.Mimetype), nil + } + + var layerNames []string + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if layer.Name != nil { + layerNames = append(layerNames, *layer.Name) + } + + } + if len(layerNames) == 0 { + return nil, errors.New("cannot get startup probe for WMS, layers could not be found") + } + + queryString := "SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=" + wms.HealthCheckBBox() + "&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=" + strings.Join(layerNames, ",") + "&STYLES=&FORMAT=image/png" + mimeType := "image/png" + return getProbe(queryString, mimeType), nil +} + +func getProbe(queryString string, mimeType string) *corev1.Probe { + probeCmd := "wget -SO- -T 10 -t 2 'http://127.0.0.1:80/mapserver?" + queryString + "' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i 'Content-Type: " + mimeType + "'" + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{Exec: &corev1.ExecAction{ + Command: []string{ + "/bin/sh", + "-c", + probeCmd, + }, + }}, + SuccessThreshold: 1, + FailureThreshold: 3, + InitialDelaySeconds: 20, + PeriodSeconds: 10, + TimeoutSeconds: 10, + } +} diff --git a/internal/controller/mapserver/deployment_test.go b/internal/controller/mapserver/deployment_test.go new file mode 100644 index 0000000..7467e8f --- /dev/null +++ b/internal/controller/mapserver/deployment_test.go @@ -0,0 +1,69 @@ +package mapserver + +import ( + "testing" + + "github.com/pdok/mapserver-operator/api/v2beta1" + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" + + _ "embed" +) + +//go:embed test_data/expected_volumemounts.yaml +var expectedVolumeMountsYaml []byte + +func TestGetVolumeMounts(t *testing.T) { + pdoknlv3.SetHost("https://service.pdok.nl") + result := getVolumeMounts(false) + + var expectedVolumeMounts struct{ VolumeMounts []corev1.VolumeMount } + err := yaml.Unmarshal(expectedVolumeMountsYaml, &expectedVolumeMounts) + assert.NoError(t, err) + assert.Equal(t, expectedVolumeMounts.VolumeMounts, result) +} + +//go:embed test_data/expected_livenessprobe.yaml +var expectedLivenessProbe []byte + +//go:embed test_data/expected_readinessprobe.yaml +var expectedReadinessProbe []byte + +//go:embed test_data/expected_startupprobe.yaml +var expectedStartupProbe []byte + +func TestGetProbesForDeployment(t *testing.T) { + var wfs = getV3() + pdoknlv3.SetHost("https://service.pdok.nl") + livenessResult, readinessResult, startupResult, err := getProbes(wfs) + assert.NoError(t, err) + + var expectedLiveness corev1.Probe + var expectedReadiness corev1.Probe + var expectedStartup corev1.Probe + err = yaml.Unmarshal(expectedLivenessProbe, &expectedLiveness) + assert.NoError(t, err) + err = yaml.Unmarshal(expectedReadinessProbe, &expectedReadiness) + assert.NoError(t, err) + err = yaml.Unmarshal(expectedStartupProbe, &expectedStartup) + assert.NoError(t, err) + assert.Equal(t, &expectedLiveness, livenessResult) + assert.Equal(t, &expectedReadiness, readinessResult) + assert.Equal(t, &expectedStartup, startupResult) +} + +//go:embed test_data/v2_input.yaml +var v2Input []byte + +func getV3() *pdoknlv3.WFS { + var v2wfs v2beta1.WFS + err := yaml.Unmarshal(v2Input, &v2wfs) + if err != nil { + panic(err) + } + var wfs pdoknlv3.WFS + _ = v2wfs.ToV3(&wfs) + return &wfs +} diff --git a/internal/controller/mapserver/test_data/expected_livenessprobe.yaml b/internal/controller/mapserver/test_data/expected_livenessprobe.yaml new file mode 100644 index 0000000..49e53e5 --- /dev/null +++ b/internal/controller/mapserver/test_data/expected_livenessprobe.yaml @@ -0,0 +1,11 @@ +exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' +failureThreshold: 3 +initialDelaySeconds: 20 +periodSeconds: 10 +successThreshold: 1 +timeoutSeconds: 10 diff --git a/internal/controller/mapserver/test_data/expected_readinessprobe.yaml b/internal/controller/mapserver/test_data/expected_readinessprobe.yaml new file mode 100644 index 0000000..5eec511 --- /dev/null +++ b/internal/controller/mapserver/test_data/expected_readinessprobe.yaml @@ -0,0 +1,11 @@ +exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=wegvakken&STARTINDEX=0&COUNT=1'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' +failureThreshold: 3 +initialDelaySeconds: 20 +periodSeconds: 10 +successThreshold: 1 +timeoutSeconds: 10 diff --git a/internal/controller/mapserver/test_data/expected_startupprobe.yaml b/internal/controller/mapserver/test_data/expected_startupprobe.yaml new file mode 100644 index 0000000..4297a3d --- /dev/null +++ b/internal/controller/mapserver/test_data/expected_startupprobe.yaml @@ -0,0 +1,11 @@ +exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=wegvakken,hectopunten&STARTINDEX=0&COUNT=1'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' +failureThreshold: 3 +initialDelaySeconds: 20 +periodSeconds: 10 +successThreshold: 1 +timeoutSeconds: 10 diff --git a/internal/controller/mapserver/test_data/expected_volumemounts.yaml b/internal/controller/mapserver/test_data/expected_volumemounts.yaml new file mode 100644 index 0000000..f0939e9 --- /dev/null +++ b/internal/controller/mapserver/test_data/expected_volumemounts.yaml @@ -0,0 +1,18 @@ +volumeMounts: +- mountPath: /srv/data + name: base + readOnly: false +- mountPath: /var/www + name: data +- mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf +- mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua +- mountPath: /srv/mapserver/config/default_mapserver.conf + name: mapserver + subPath: default_mapserver.conf +- mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml diff --git a/internal/controller/mapserver/test_data/v2_input.yaml b/internal/controller/mapserver/test_data/v2_input.yaml new file mode 100644 index 0000000..d4dfc87 --- /dev/null +++ b/internal/controller/mapserver/test_data/v2_input.yaml @@ -0,0 +1,162 @@ +apiVersion: pdok.nl/v2beta1 +kind: WFS +metadata: + name: rws-nwbwegen-v1-0 + labels: + dataset-owner: rws + dataset: nwbwegen + service-version: v1_0 + service-type: wfs + annotations: + lifecycle-phase: prod + service-bundle-id: b39c152b-393b-52f5-a50c-e1ffe904b6fb +spec: + general: + datasetOwner: rws + dataset: nwbwegen + serviceVersion: v1_0 + kubernetes: + resources: + limits: + ephemeralStorage: 505Mi + requests: + ephemeralStorage: 255Mi + service: + title: NWB - Wegen WFS + abstract: + Dit is de web feature service van het Nationaal Wegen Bestand (NWB) + - wegen. Deze dataset bevat alleen de wegvakken en hectometerpunten. Het Nationaal + Wegen Bestand - Wegen is een digitaal geografisch bestand van alle wegen in + Nederland. Opgenomen zijn alle wegen die worden beheerd door wegbeheerders als + het Rijk, provincies, gemeenten en waterschappen, echter alleen voor zover deze + zijn voorzien van een straatnaam of nummer. + inspire: true + metadataIdentifier: a9fa7fff-6365-4885-950c-e9d9848359ee + authority: + name: rws + url: https://www.rijkswaterstaat.nl + dataEPSG: EPSG:28992 + extent: -59188.44333693248 304984.64144318487 308126.88473339565 858328.516489961 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + - Hectometerpunten + - HVD + - Mobiliteit + featureTypes: + - name: wegvakken + title: Wegvakken + abstract: + Dit featuretype bevat de wegvakken uit het Nationaal Wegen bestand + (NWB) en bevat gedetailleerde informatie per wegvak zoals straatnaam, wegnummer, + routenummer, wegbeheerder, huisnummers, enz. + sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Wegvakken + data: + gpkg: + table: wegvakken + geometryType: MultiLineString + blobKey: geopackages/rws/nwbwegen/1c56dc48-2cf4-4631-8b09-ed385d5368d1/1/nwb_wegen.gpkg + columns: + - fid + - objectid + - wvk_id + - wvk_begdat + - jte_id_beg + - jte_id_end + - wegbehsrt + - wegnummer + - wegdeelltr + - hecto_lttr + - bst_code + - rpe_code + - admrichtng + - rijrichtng + - stt_naam + - stt_bron + - wpsnaam + - gme_id + - gme_naam + - hnrstrlnks + - hnrstrrhts + - e_hnr_lnks + - e_hnr_rhts + - l_hnr_lnks + - l_hnr_rhts + - begafstand + - endafstand + - beginkm + - eindkm + - pos_tv_wol + - wegbehcode + - wegbehnaam + - distrcode + - distrnaam + - dienstcode + - dienstnaam + - wegtype + - wgtype_oms + - routeltr + - routenr + - routeltr2 + - routenr2 + - routeltr3 + - routenr3 + - routeltr4 + - routenr4 + - wegnr_aw + - wegnr_hmp + - geobron_id + - geobron_nm + - bronjaar + - openlr + - bag_orl + - frc + - fow + - alt_naam + - alt_nr + - rel_hoogte + - st_lengthshape + - name: hectopunten + title: Hectopunten + abstract: + Dit featuretype bevat de hectopunten uit het Nationaal Wegen Bestand + (NWB) en bevat gedetailleerde informatie per hectopunt zoals hectometrering, + afstand, zijde en hectoletter. + sourceMetadataIdentifier: 8f0497f0-dbd7-4bee-b85a-5fdec484a7ff + datasetMetadataIdentifier: a9b7026e-0a81-4813-93bd-ba49e6f28502 + keywords: + - Vervoersnetwerken + - Menselijke gezondheid en veiligheid + - Geluidsbelasting hoofdwegen (Richtlijn Omgevingslawaai) + - Nationaal + - Voertuigen + - Verkeer + - Hectometerpunten + data: + gpkg: + blobKey: geopackages/rws/nwbwegen/1c56dc48-2cf4-4631-8b09-ed385d5368d1/1/nwb_wegen.gpkg + columns: + - fid + - objectid + - hectomtrng + - afstand + - wvk_id + - wvk_begdat + - zijde + - hecto_lttr + geometryType: MultiPoint + table: hectopunten diff --git a/internal/controller/middleware.go b/internal/controller/middleware.go new file mode 100644 index 0000000..67efa86 --- /dev/null +++ b/internal/controller/middleware.go @@ -0,0 +1,48 @@ +package controller + +import ( + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + "github.com/traefik/traefik/v3/pkg/config/dynamic" + traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +const corsHeadersName = "mapserver-headers" + +func getBareCorsHeadersMiddleware[O pdoknlv3.WMSWFS](obj O) *traefikiov1alpha1.Middleware { + return &traefikiov1alpha1.Middleware{ + ObjectMeta: metav1.ObjectMeta{ + Name: getSuffixedName(obj, corsHeadersName), + // name might become too long. not handling here. will just fail on apply. + Namespace: obj.GetNamespace(), + UID: obj.GetUID(), + }, + } +} + +func mutateCorsHeadersMiddleware[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, middleware *traefikiov1alpha1.Middleware) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, middleware, labels); err != nil { + return err + } + middleware.Spec = traefikiov1alpha1.MiddlewareSpec{ + Headers: &dynamic.Headers{ + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Headers": "Content-Type", + "Access-Control-Allow-Method": "GET, POST, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "public, max-age=3600, no-transform", + }, + }, + } + + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, middleware, middleware); err != nil { + return err + } + + return ctrl.SetControllerReference(obj, middleware, getReconcilerScheme(r)) +} diff --git a/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go b/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go new file mode 100644 index 0000000..408d83d --- /dev/null +++ b/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy.go @@ -0,0 +1,93 @@ +package ogcwebserviceproxy + +import ( + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/constants" + "github.com/pdok/mapserver-operator/internal/controller/types" + "github.com/pdok/mapserver-operator/internal/controller/utils" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + yaml "sigs.k8s.io/yaml/goyaml.v3" +) + +func GetOgcWebserviceProxyContainer(wms *pdoknlv3.WMS, images types.Images) (*corev1.Container, error) { + container := corev1.Container{ + Name: constants.OgcWebserviceProxyName, + Image: images.OgcWebserviceProxyImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{{ContainerPort: 9111}}, + Command: getCommand(wms), + VolumeMounts: []corev1.VolumeMount{ + utils.GetConfigVolumeMount(constants.ConfigMapOgcWebserviceProxyVolumeName), + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("200M"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("0.05"), + }, + }, + } + return &container, nil +} + +func getCommand(wms *pdoknlv3.WMS) []string { + command := []string{ + "/ogc-webservice-proxy", + "-h=http://127.0.0.1/", + "-t=wms", + "-s=/input/service-config.yaml", + } + + if wms.Options().ValidateRequests { + command = append(command, "-v") + } + if wms.Options().RewriteGroupToDataLayers { + command = append(command, "-r") + } + + command = append(command, "-d=15") + return command + +} + +func GetConfig(wms *pdoknlv3.WMS) (config string, err error) { + input, err := MapWMSToOgcWebserviceProxyConfig(wms) + if err != nil { + return "", err + } + + yamlConfig, err := yaml.Marshal(input) + if err != nil { + return "", err + } + return string(yamlConfig), nil +} + +func MapWMSToOgcWebserviceProxyConfig(wms *pdoknlv3.WMS) (config Config, err error) { + dataLayersForGroupLayer := func(l pdoknlv3.Layer) []string { + var dataLayers []string + for _, childLayer := range l.GetAllSublayers() { + if childLayer.IsDataLayer() { + dataLayers = append(dataLayers, *childLayer.Name) + } + } + return dataLayers + } + + config.GroupLayers = make(map[string][]string) + for _, layer := range wms.Spec.Service.GetAnnotatedLayers() { + if !layer.IsTopLayer && layer.IsGroupLayer && layer.Name != nil { + config.GroupLayers[*layer.Name] = dataLayersForGroupLayer(layer.Layer) + } + } + if wms.Spec.Service.Layer.Name != nil { + config.GroupLayers[*wms.Spec.Service.Layer.Name] = dataLayersForGroupLayer(wms.Spec.Service.Layer) + } + return +} + +type Config struct { + GroupLayers map[string][]string `yaml:"grouplayers"` +} diff --git a/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go b/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go new file mode 100644 index 0000000..3cda6b2 --- /dev/null +++ b/internal/controller/ogcwebserviceproxy/ogc_webservice_proxy_test.go @@ -0,0 +1,46 @@ +package ogcwebserviceproxy + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/yaml" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" +) + +func TestGetConfig(t *testing.T) { + tests := []string{"named-toplayer", "unnamed-toplayer"} + + for _, tt := range tests { + input, err := os.ReadFile("test_data/input/" + tt + ".yaml") + if err != nil { + t.Errorf("os.ReadFile() error = %v", err) + } + wms := &pdoknlv3.WMS{} + if err := yaml.Unmarshal(input, wms); err != nil { + t.Errorf("yaml.Unmarshal() error = %v", err) + } + + generated, err := MapWMSToOgcWebserviceProxyConfig(wms) + if err != nil { + t.Errorf("MapWMSToOgcWebserviceProxyConfig() error = %v", err) + } + + expectedBytes, err := os.ReadFile("test_data/expected/" + tt + ".yaml") + if err != nil { + t.Errorf("os.ReadFile() error = %v", err) + } + + var expected Config + if err := yaml.Unmarshal(expectedBytes, &expected); err != nil { + t.Errorf("yaml.Unmarshal() error = %v", err) + } + + diff := cmp.Diff(expected, generated) + if diff != "" { + t.Errorf("GetConfig() mismatch (-want +got):\n%s", diff) + } + } +} diff --git a/internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml new file mode 100644 index 0000000..e867543 --- /dev/null +++ b/internal/controller/ogcwebserviceproxy/test_data/expected/named-toplayer.yaml @@ -0,0 +1,12 @@ +grouplayers: + grouplayer-1: + - datalayer-1 + - datalayer-2 + grouplayer-2: + - datalayer-3 + - datalayer-4 + toplayer: + - datalayer-1 + - datalayer-2 + - datalayer-3 + - datalayer-4 \ No newline at end of file diff --git a/internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml new file mode 100644 index 0000000..d1b7dee --- /dev/null +++ b/internal/controller/ogcwebserviceproxy/test_data/expected/unnamed-toplayer.yaml @@ -0,0 +1,7 @@ +grouplayers: + grouplayer-1: + - datalayer-1 + - datalayer-2 + grouplayer-2: + - datalayer-3 + - datalayer-4 \ No newline at end of file diff --git a/internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml new file mode 100644 index 0000000..8df4892 --- /dev/null +++ b/internal/controller/ogcwebserviceproxy/test_data/input/named-toplayer.yaml @@ -0,0 +1,52 @@ +metadata: +spec: + service: + abstract: "" + dataEPSG: "" + keywords: null + layer: + layers: + - layers: + - data: + gpkg: + blobKey: blob-1 + columns: null + geometryType: "" + tableName: "" + name: datalayer-1 + visible: false + - data: + gpkg: + blobKey: blob-2 + columns: null + geometryType: "" + tableName: "" + name: datalayer-2 + visible: false + name: grouplayer-1 + visible: false + - layers: + - data: + gpkg: + blobKey: blob-3 + columns: null + geometryType: "" + tableName: "" + name: datalayer-3 + visible: false + - data: + gpkg: + blobKey: blob-4 + columns: null + geometryType: "" + tableName: "" + name: datalayer-4 + visible: false + name: grouplayer-2 + visible: false + name: toplayer + visible: false + ownerInfoRef: "" + prefix: "" + title: "" + url: "http://test.test/test" diff --git a/internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml b/internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml new file mode 100644 index 0000000..f34f687 --- /dev/null +++ b/internal/controller/ogcwebserviceproxy/test_data/input/unnamed-toplayer.yaml @@ -0,0 +1,51 @@ +metadata: +spec: + service: + abstract: "" + dataEPSG: "" + keywords: null + layer: + layers: + - layers: + - data: + gpkg: + blobKey: blob-1 + columns: null + geometryType: "" + tableName: "" + name: datalayer-1 + visible: false + - data: + gpkg: + blobKey: blob-2 + columns: null + geometryType: "" + tableName: "" + name: datalayer-2 + visible: false + name: grouplayer-1 + visible: false + - layers: + - data: + gpkg: + blobKey: blob-3 + columns: null + geometryType: "" + tableName: "" + name: datalayer-3 + visible: false + - data: + gpkg: + blobKey: blob-4 + columns: null + geometryType: "" + tableName: "" + name: datalayer-4 + visible: false + name: grouplayer-2 + visible: false + visible: false + ownerInfoRef: "" + prefix: "" + title: "" + url: "http://test.test/test" diff --git a/internal/controller/poddisruptionbudget.go b/internal/controller/poddisruptionbudget.go new file mode 100644 index 0000000..6bd848b --- /dev/null +++ b/internal/controller/poddisruptionbudget.go @@ -0,0 +1,42 @@ +package controller + +import ( + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/constants" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" +) + +func getBarePodDisruptionBudget[O pdoknlv3.WMSWFS](obj O) *policyv1.PodDisruptionBudget { + return &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: getSuffixedName(obj, constants.MapserverName), + Namespace: obj.GetNamespace(), + }, + } +} + +func mutatePodDisruptionBudget[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, podDisruptionBudget *policyv1.PodDisruptionBudget) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, podDisruptionBudget, labels); err != nil { + return err + } + + matchLabels := smoothoperatorutils.CloneOrEmptyMap(labels) + podDisruptionBudget.Spec = policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}, + Selector: &metav1.LabelSelector{ + MatchLabels: matchLabels, + }, + } + + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, podDisruptionBudget, podDisruptionBudget); err != nil { + return err + } + return ctrl.SetControllerReference(obj, podDisruptionBudget, getReconcilerScheme(r)) +} diff --git a/internal/controller/reconciler.go b/internal/controller/reconciler.go new file mode 100644 index 0000000..bba708e --- /dev/null +++ b/internal/controller/reconciler.go @@ -0,0 +1,45 @@ +package controller + +import ( + "github.com/pdok/mapserver-operator/internal/controller/types" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type Reconciler interface { + *WFSReconciler | *WMSReconciler + client.StatusClient +} + +func getReconcilerClient[R Reconciler](r R) client.Client { + switch any(r).(type) { + case *WFSReconciler: + return any(r).(*WFSReconciler).Client + case *WMSReconciler: + return any(r).(*WMSReconciler).Client + } + + return nil +} + +func getReconcilerScheme[R Reconciler](r R) *runtime.Scheme { + switch any(r).(type) { + case *WFSReconciler: + return any(r).(*WFSReconciler).Scheme + case *WMSReconciler: + return any(r).(*WMSReconciler).Scheme + } + + return nil +} + +func getReconcilerImages[R Reconciler](r R) *types.Images { + switch any(r).(type) { + case *WFSReconciler: + return &any(r).(*WFSReconciler).Images + case *WMSReconciler: + return &any(r).(*WMSReconciler).Images + } + + return nil +} diff --git a/internal/controller/service.go b/internal/controller/service.go new file mode 100644 index 0000000..fae666d --- /dev/null +++ b/internal/controller/service.go @@ -0,0 +1,77 @@ +package controller + +import ( + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/constants" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + mapserverWebserviceProxyPortNr = 9111 + metricPortName = "metric" +) + +func getBareService[O pdoknlv3.WMSWFS](obj O) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: getSuffixedName(obj, constants.MapserverName), + Namespace: obj.GetNamespace(), + }, + } +} + +func mutateService[R Reconciler, O pdoknlv3.WMSWFS](r R, obj O, service *corev1.Service) error { + reconcilerClient := getReconcilerClient(r) + + labels := addCommonLabels(obj, smoothoperatorutils.CloneOrEmptyMap(obj.GetLabels())) + selector := smoothoperatorutils.CloneOrEmptyMap(labels) + if err := smoothoperatorutils.SetImmutableLabels(reconcilerClient, service, labels); err != nil { + return err + } + + ports := []corev1.ServicePort{ + { + Name: constants.MapserverName, + Port: constants.MapserverPortNr, + TargetPort: intstr.FromInt32(constants.MapserverPortNr), + Protocol: corev1.ProtocolTCP, + }, + } + + if obj.Type() == pdoknlv3.ServiceTypeWMS { + if obj.Options().UseWebserviceProxy() { + ports = append(ports, corev1.ServicePort{ + Name: constants.OgcWebserviceProxyName, + Port: 9111, + }) + } + } + + // Add port here to get the same port order as the odl ansible operator + ports = append(ports, corev1.ServicePort{ + Name: metricPortName, + Port: constants.ApachePortNr, + TargetPort: intstr.FromInt32(constants.ApachePortNr), + Protocol: corev1.ProtocolTCP, + }) + + service.Spec = corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: service.Spec.ClusterIP, + ClusterIPs: service.Spec.ClusterIPs, + IPFamilyPolicy: service.Spec.IPFamilyPolicy, + IPFamilies: service.Spec.IPFamilies, + SessionAffinity: corev1.ServiceAffinityNone, + InternalTrafficPolicy: smoothoperatorutils.Pointer(corev1.ServiceInternalTrafficPolicyCluster), + Ports: ports, + Selector: selector, + } + if err := smoothoperatorutils.EnsureSetGVK(reconcilerClient, service, service); err != nil { + return err + } + return ctrl.SetControllerReference(obj, service, getReconcilerScheme(r)) +} diff --git a/internal/controller/shared_controller.go b/internal/controller/shared_controller.go new file mode 100644 index 0000000..b70d5a7 --- /dev/null +++ b/internal/controller/shared_controller.go @@ -0,0 +1,306 @@ +package controller + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/pkg/errors" + + traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + policyv1 "k8s.io/api/policy/v1" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/pdok/smooth-operator/model" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "github.com/pdok/mapserver-operator/internal/controller/types" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorstatus "github.com/pdok/smooth-operator/pkg/status" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + AppLabelKey = "pdok.nl/app" + InspireLabelKey = "pdok.nl/inspire" +) + +func createControllerManager(mgr ctrl.Manager, obj client.Object) *builder.TypedBuilder[reconcile.Request] { + var kind string + switch any(obj).(type) { + case *pdoknlv3.WMS: + kind = "WMS" + case *pdoknlv3.WFS: + kind = "WFS" + } + + controllerMgr := ctrl.NewControllerManagedBy(mgr).For(obj).Named(strings.ToLower(kind)) + controllerMgr.Owns(&corev1.ConfigMap{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&appsv1.Deployment{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&traefikiov1alpha1.Middleware{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&traefikiov1alpha1.IngressRoute{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&autoscalingv2.HorizontalPodAutoscaler{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&policyv1.PodDisruptionBudget{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&smoothoperatorv1.OwnerInfo{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})) + + return controllerMgr.Watches(&appsv1.ReplicaSet{}, smoothoperatorstatus.GetReplicaSetEventHandlerForObj(mgr, kind)) +} + +func ttlExpired[O pdoknlv3.WMSWFS](obj O) bool { + var lifecycle *model.Lifecycle + switch any(obj).(type) { + case *pdoknlv3.WFS: + wfs := any(obj).(*pdoknlv3.WFS) + lifecycle = wfs.Spec.Lifecycle + case *pdoknlv3.WMS: + wms := any(obj).(*pdoknlv3.WMS) + lifecycle = wms.Spec.Lifecycle + } + + if lifecycle != nil && lifecycle.TTLInDays != nil { + expiresAt := obj.GetCreationTimestamp().Add(time.Duration(*lifecycle.TTLInDays) * 24 * time.Hour) + + return expiresAt.Before(time.Now()) + } + + return false +} + +func ensureLabel[O pdoknlv3.WMSWFS](obj O, key, value string) { + labels := obj.GetLabels() + if _, ok := labels[key]; !ok { + labels[key] = value + } + + obj.SetLabels(labels) +} + +func getSuffixedName[O pdoknlv3.WMSWFS](obj O, suffix string) string { + return obj.TypedName() + "-" + suffix +} + +func addCommonLabels[O pdoknlv3.WMSWFS](obj O, labels map[string]string) map[string]string { + labels[AppLabelKey] = constants.MapserverName + + inspire := false + switch any(obj).(type) { + case *pdoknlv3.WFS: + inspire = any(obj).(*pdoknlv3.WFS).Spec.Service.Inspire != nil + case *pdoknlv3.WMS: + inspire = any(obj).(*pdoknlv3.WMS).Spec.Service.Inspire != nil + } + + labels[InspireLabelKey] = strconv.FormatBool(inspire) + + return labels +} + +func createOrUpdateAllForWMSWFS[R Reconciler, O pdoknlv3.WMSWFS](ctx context.Context, r R, obj O, ownerInfo *smoothoperatorv1.OwnerInfo) (operationResults map[string]controllerutil.OperationResult, err error) { + reconcilerClient := getReconcilerClient(r) + + hashedConfigMapNames, operationResults, err := createOrUpdateConfigMaps(ctx, r, obj, ownerInfo) + if err != nil { + return operationResults, err + } + + // region Deployment + { + deployment := getBareDeployment(obj) + operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, deployment)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, deployment, func() error { + return mutateDeployment(r, obj, deployment, hashedConfigMapNames) + }) + if err != nil && !strings.Contains(err.Error(), "the object has been modified; please apply your changes to the latest version and try again") { + return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, deployment), err) + } + } + // end region Deployment + + // region TraefikMiddleware + if obj.Options().IncludeIngress { + middleware := getBareCorsHeadersMiddleware(obj) + operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, middleware)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, middleware, func() error { + return mutateCorsHeadersMiddleware(r, obj, middleware) + }) + if err != nil { + return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, middleware), err) + } + } + // end region TraefikMiddleware + + // region PodDisruptionBudget + { + err = createOrUpdateOrDeletePodDisruptionBudget(ctx, r, obj, operationResults) + if err != nil { + return operationResults, err + } + } + // end region PodDisruptionBudget + + // region HorizontalAutoScaler + { + autoscaler := getBareHorizontalPodAutoScaler(obj) + operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, autoscaler)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, autoscaler, func() error { + return mutateHorizontalPodAutoscaler(r, obj, autoscaler) + }) + if err != nil { + return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, autoscaler), err) + } + } + // end region HorizontalAutoScaler + + // region IngressRoute + if obj.Options().IncludeIngress { + ingress := getBareIngressRoute(obj) + operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, ingress)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, ingress, func() error { + return mutateIngressRoute(r, obj, ingress) + }) + if err != nil { + return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, ingress), err) + } + } + // end region IngressRoute + + // region Service + { + service := getBareService(obj) + operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, service)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, service, func() error { + return mutateService(r, obj, service) + }) + if err != nil { + return operationResults, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, service), err) + } + } + // end region Service + + return operationResults, nil +} + +func createOrUpdateConfigMaps[R Reconciler, O pdoknlv3.WMSWFS](ctx context.Context, r R, obj O, ownerInfo *smoothoperatorv1.OwnerInfo) (hashedConfigMapNames types.HashedConfigMapNames, operationResults map[string]controllerutil.OperationResult, err error) { + operationResults, configMaps := make(map[string]controllerutil.OperationResult), make(map[string]func(R, O, *corev1.ConfigMap) error) + configMaps[constants.MapserverName] = mutateConfigMap + if obj.Mapfile() == nil { + configMaps[constants.MapfileGeneratorName] = func(r R, o O, cm *corev1.ConfigMap) error { + return mutateConfigMapMapfileGenerator(r, o, cm, ownerInfo) + } + } + configMaps[constants.CapabilitiesGeneratorName] = func(r R, o O, cm *corev1.ConfigMap) error { + return mutateConfigMapCapabilitiesGenerator(r, o, cm, ownerInfo) + } + if obj.Options().PrefetchData { + configMaps[constants.InitScriptsName] = mutateConfigMapBlobDownload + } + if obj.Type() == pdoknlv3.ServiceTypeWMS { + wms, _ := any(obj).(*pdoknlv3.WMS) + wmsReconciler := (*WMSReconciler)(r) + + configMaps[constants.LegendGeneratorName] = func(_ R, _ O, cm *corev1.ConfigMap) error { + return mutateConfigMapLegendGenerator(wmsReconciler, wms, cm) + } + configMaps[constants.FeatureinfoGeneratorName] = func(_ R, _ O, cm *corev1.ConfigMap) error { + return mutateConfigMapFeatureinfoGenerator(wmsReconciler, wms, cm) + } + configMaps[constants.OgcWebserviceProxyName] = func(_ R, _ O, cm *corev1.ConfigMap) error { + return mutateConfigMapOgcWebserviceProxy(wmsReconciler, wms, cm) + } + } + for cmName, mutate := range configMaps { + cm, or, err := createOrUpdateConfigMap(ctx, obj, r, cmName, func(r R, o O, cm *corev1.ConfigMap) error { + return mutate(r, o, cm) + }) + if or != nil { + operationResults[smoothoperatorutils.GetObjectFullName(getReconcilerClient(r), cm)] = *or + } + if err != nil { + return hashedConfigMapNames, operationResults, err + } + switch cmName { + case constants.MapserverName: + hashedConfigMapNames.Mapserver = cm.Name + case constants.MapfileGeneratorName: + hashedConfigMapNames.MapfileGenerator = cm.Name + case constants.CapabilitiesGeneratorName: + hashedConfigMapNames.CapabilitiesGenerator = cm.Name + case constants.InitScriptsName: + hashedConfigMapNames.InitScripts = cm.Name + case constants.LegendGeneratorName: + hashedConfigMapNames.LegendGenerator = cm.Name + case constants.FeatureinfoGeneratorName: + hashedConfigMapNames.FeatureInfoGenerator = cm.Name + case constants.OgcWebserviceProxyName: + hashedConfigMapNames.OgcWebserviceProxy = cm.Name + } + } + + return hashedConfigMapNames, operationResults, err +} + +func createOrUpdateConfigMap[O pdoknlv3.WMSWFS, R Reconciler](ctx context.Context, obj O, reconciler R, name string, mutate func(R, O, *corev1.ConfigMap) error) (*corev1.ConfigMap, *controllerutil.OperationResult, error) { + reconcilerClient := getReconcilerClient(reconciler) + cm := getBareConfigMap(obj, name) + if err := mutate(reconciler, obj, cm); err != nil { + return cm, nil, err + } + or, err := controllerutil.CreateOrUpdate(ctx, reconcilerClient, cm, func() error { + return mutate(reconciler, obj, cm) + }) + if err != nil { + return cm, &or, fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, cm), err) + } + return cm, &or, nil +} + +func createOrUpdateOrDeletePodDisruptionBudget[O pdoknlv3.WMSWFS, R Reconciler](ctx context.Context, reconciler R, obj O, operationResults map[string]controllerutil.OperationResult) (err error) { + reconcilerClient := getReconcilerClient(reconciler) + podDisruptionBudget := getBarePodDisruptionBudget(obj) + autoscalerPatch := obj.HorizontalPodAutoscalerPatch() + if autoscalerPatch != nil && autoscalerPatch.MinReplicas != nil && autoscalerPatch.MaxReplicas != nil && + *autoscalerPatch.MinReplicas == 1 && *autoscalerPatch.MaxReplicas == 1 { + err = reconcilerClient.Delete(ctx, podDisruptionBudget) + if err == nil { + operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget)] = "deleted" + } + if client.IgnoreNotFound(err) != nil { + return fmt.Errorf("unable to delete resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget), err) + } + } else { + operationResults[smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget)], err = controllerutil.CreateOrUpdate(ctx, reconcilerClient, podDisruptionBudget, func() error { + return mutatePodDisruptionBudget(reconciler, obj, podDisruptionBudget) + }) + if err != nil { + return fmt.Errorf("unable to create/update resource %s: %w", smoothoperatorutils.GetObjectFullName(reconcilerClient, podDisruptionBudget), err) + } + } + return nil +} + +func recoveredPanicToError(rec any) (err error) { + switch x := rec.(type) { + case string: + err = errors.New(x) + case error: + err = x + default: + err = errors.New("unknown panic") + } + + // Add stack + // TODO - this doesn't seem to work, see if there is a better method to add the stack + err = errors.WithStack(err) + + return +} diff --git a/internal/controller/shared_controller_test.go b/internal/controller/shared_controller_test.go new file mode 100644 index 0000000..2d32671 --- /dev/null +++ b/internal/controller/shared_controller_test.go @@ -0,0 +1,456 @@ +package controller + +import ( + "context" + "fmt" + "os" + "slices" + "strings" + "testing" + + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + "github.com/stretchr/testify/assert" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + "github.com/google/go-cmp/cmp" + "github.com/pdok/mapserver-operator/api/v2beta1" + "github.com/pdok/mapserver-operator/internal/controller/types" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" + traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + v2 "k8s.io/api/autoscaling/v2" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd + . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + namespace = "default" + testImageName1 = "test.test/image:test1" + testImageName2 = "test.test/image:test2" + testImageName3 = "test.test/image:test3" + testImageName4 = "test.test/image:test4" + testImageName5 = "test.test/image:test5" + testImageName6 = "test.test/image:test6" + testImageName7 = "test.test/image:test7" +) + +func getHashedConfigMapNameFromClient[O pdoknlv3.WMSWFS](ctx context.Context, obj O, volumeName string) (string, error) { + deployment := &appsv1.Deployment{} + err := k8sClient.Get(ctx, k8stypes.NamespacedName{Namespace: obj.GetNamespace(), Name: getBareDeployment(obj).GetName()}, deployment) + if err != nil { + return "", err + } + + for _, volume := range deployment.Spec.Template.Spec.Volumes { + if volume.Name == volumeName && volume.ConfigMap != nil { + return volume.ConfigMap.Name, nil + } + } + return "", fmt.Errorf("configmap %s not found", volumeName) +} + +func getExpectedObjects[O pdoknlv3.WMSWFS](ctx context.Context, obj O, includeBlobDownload bool, includeMapfileGeneratorConfigMap bool) ([]client.Object, error) { + objects := []client.Object{ + getBareDeployment(obj), + getBareHorizontalPodAutoScaler(obj), + getBareService(obj), + getBareIngressRoute(obj), + getBareCorsHeadersMiddleware(obj), + getBarePodDisruptionBudget(obj), + } + + // Add all ConfigMaps with hashed names + cm := getBareConfigMap(obj, constants.MapserverName) + hashedName, err := getHashedConfigMapNameFromClient(ctx, obj, constants.MapserverName) + if err != nil { + return objects, err + } + cm.Name = hashedName + objects = append(objects, cm) + + if includeMapfileGeneratorConfigMap { + cm = getBareConfigMap(obj, constants.MapfileGeneratorName) + hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapMapfileGeneratorVolumeName) + if err != nil { + return objects, err + } + cm.Name = hashedName + objects = append(objects, cm) + } + + cm = getBareConfigMap(obj, constants.CapabilitiesGeneratorName) + hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapCapabilitiesGeneratorVolumeName) + if err != nil { + return objects, err + } + cm.Name = hashedName + objects = append(objects, cm) + + if includeBlobDownload { + cm = getBareConfigMap(obj, constants.InitScriptsName) + hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.InitScriptsName) + if err != nil { + return objects, err + } + cm.Name = hashedName + objects = append(objects, cm) + } + + if obj.Type() == pdoknlv3.ServiceTypeWMS { + wms, _ := any(obj).(*pdoknlv3.WMS) + cm = getBareConfigMap(wms, constants.LegendGeneratorName) + hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapLegendGeneratorVolumeName) + if err != nil { + return objects, err + } + cm.Name = hashedName + objects = append(objects, cm) + + cm = getBareConfigMap(wms, constants.FeatureinfoGeneratorName) + hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapFeatureinfoGeneratorVolumeName) + if err != nil { + return objects, err + } + cm.Name = hashedName + objects = append(objects, cm) + + if obj.Options().UseWebserviceProxy() { + cm = getBareConfigMap(wms, constants.OgcWebserviceProxyName) + hashedName, err = getHashedConfigMapNameFromClient(ctx, obj, constants.ConfigMapOgcWebserviceProxyVolumeName) + if err != nil { + return objects, err + } + cm.Name = hashedName + objects = append(objects, cm) + } + } + + return objects, nil +} + +func testPath(t pdoknlv3.ServiceType, test string) string { + return fmt.Sprintf("test_data/%s/%s/", strings.ToLower(string(t)), test) +} + +func testMutate[T any](kind string, result *T, expectedFile string, mutate func(*T) error) { + By("Testing mutating the " + kind) + err := mutate(result) + Expect(err).NotTo(HaveOccurred()) + + var expected T + data, err := os.ReadFile(expectedFile) + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, &expected) + Expect(err).NotTo(HaveOccurred()) + + diff := cmp.Diff(expected, *result) + if diff != "" { + Fail(diff) + } + + By(fmt.Sprintf("Testing mutating the %s twice has the same result", kind)) + generated := *result + err = mutate(result) + Expect(err).NotTo(HaveOccurred()) + diff = cmp.Diff(generated, *result) + if diff != "" { + Fail(diff) + } +} + +//nolint:unparam +func testMutateConfigMap(m *corev1.ConfigMap, expectedFile string, mutate func(*corev1.ConfigMap) error, ignoreValues bool) { + clearConfigMapValues := func(cm *corev1.ConfigMap) { + newMap := map[string]string{} + for k := range cm.Data { + newMap[k] = "IGNORED" + } + cm.Data = newMap + } + + if !ignoreValues { + testMutate("ConfigMap", m, expectedFile, mutate) + } else { + By("Testing mutating the ConfigMap") + err := mutate(m) + Expect(err).NotTo(HaveOccurred()) + + expected := &corev1.ConfigMap{} + data, err := os.ReadFile(expectedFile) + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, expected) + Expect(err).NotTo(HaveOccurred()) + + c := m.DeepCopy() + clearConfigMapValues(c) + clearConfigMapValues(expected) + + diff := cmp.Diff(*expected, *c) + if diff != "" { + Fail(diff) + } + } +} + +func testMutates[R Reconciler, O pdoknlv3.WMSWFS](reconcilerFn func() R, resource O, name string, ignoreFiles ...string) { + inputPath := testPath(resource.Type(), name) + "input/" + outputPath := testPath(resource.Type(), name) + "expected/" + + shouldIncludeFile := func(name string) (string, bool) { + if slices.Contains(ignoreFiles, name) { + return "", false + } + + return outputPath + name, true + } + + var fileName string + switch resource.Type() { + case pdoknlv3.ServiceTypeWFS: + fileName = "wfs.yaml" + case pdoknlv3.ServiceTypeWMS: + fileName = "wms.yaml" + default: + panic("unknown servicetype") + } + + owner := smoothoperatorv1.OwnerInfo{} + + It("Should parse the input files correctly", func() { + data, err := readTestFile(inputPath + fileName) + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, &resource) + Expect(err).NotTo(HaveOccurred()) + Expect(resource.GetName()).Should(Equal(name)) + + data, err = os.ReadFile(inputPath + "ownerinfo.yaml") + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, &owner) + Expect(err).NotTo(HaveOccurred()) + Expect(owner.Name).Should(Equal("owner")) + + Expect(k8sClient.Create(ctx, &owner)).To(Succeed()) + + var validationError error + switch any(resource).(type) { + case *pdoknlv3.WMS: + wms := any(resource).(*pdoknlv3.WMS) + _, validationError = wms.ValidateCreate(k8sClient) + case *pdoknlv3.WFS: + wfs := any(resource).(*pdoknlv3.WFS) + _, validationError = wfs.ValidateCreate(k8sClient) + } + Expect(validationError).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, &owner)).To(Succeed()) + }) + + configMapNames := types.HashedConfigMapNames{} + + It("Should generate a correct Configmap", func() { + cm := getBareConfigMap(resource, constants.MapserverName) + testMutateConfigMap(cm, outputPath+"configmap-mapserver.yaml", func(cm *corev1.ConfigMap) error { + return mutateConfigMap(reconcilerFn(), resource, cm) + }, true) + configMapNames.Mapserver = cm.Name + }) + + It("Should generate a correct BlobDownload Configmap", func() { + if path, include := shouldIncludeFile("configmap-init-scripts.yaml"); include { + cm := getBareConfigMap(resource, constants.InitScriptsName) + testMutateConfigMap(cm, path, func(cm *corev1.ConfigMap) error { + return mutateConfigMapBlobDownload(reconcilerFn(), resource, cm) + }, true) + configMapNames.InitScripts = cm.Name + } + }) + + It("Should generate a correct MapfileGenerator Configmap", func() { + if path, include := shouldIncludeFile("configmap-mapfile-generator.yaml"); include { + cm := getBareConfigMap(resource, constants.MapfileGeneratorName) + testMutateConfigMap(cm, path, func(cm *corev1.ConfigMap) error { + return mutateConfigMapMapfileGenerator(reconcilerFn(), resource, cm, &owner) + }, true) + configMapNames.MapfileGenerator = cm.Name + } + }) + + It("Should generate a correct CapabilitiesGenerator Configmap", func() { + cm := getBareConfigMap(resource, constants.CapabilitiesGeneratorName) + testMutateConfigMap(cm, outputPath+"configmap-capabilities-generator.yaml", func(cm *corev1.ConfigMap) error { + return mutateConfigMapCapabilitiesGenerator(reconcilerFn(), resource, cm, &owner) + }, true) + configMapNames.CapabilitiesGenerator = cm.Name + }) + + if resource.Type() == pdoknlv3.ServiceTypeWMS { + wms := any(resource).(*pdoknlv3.WMS) + It("Should generate a correct FeatureInfo Configmap", func() { + cm := getBareConfigMap(resource, constants.FeatureinfoGeneratorName) + testMutateConfigMap(cm, outputPath+"configmap-featureinfo-generator.yaml", func(cm *corev1.ConfigMap) error { + return mutateConfigMapFeatureinfoGenerator(getWMSReconciler(), wms, cm) + }, true) + configMapNames.FeatureInfoGenerator = cm.Name + }) + + It("Should generate a correct LegendGenerator Configmap", func() { + cm := getBareConfigMap(resource, constants.LegendGeneratorName) + testMutateConfigMap(cm, outputPath+"configmap-legend-generator.yaml", func(cm *corev1.ConfigMap) error { + return mutateConfigMapLegendGenerator(getWMSReconciler(), wms, cm) + }, true) + configMapNames.LegendGenerator = cm.Name + }) + + It("Should generate a correct OGC webservice proxy Configmap", func() { + cm := getBareConfigMap(resource, constants.OgcWebserviceProxyName) + testMutateConfigMap(cm, outputPath+"configmap-ogc-webservice-proxy.yaml", func(cm *corev1.ConfigMap) error { + return mutateConfigMapOgcWebserviceProxy(getWMSReconciler(), wms, cm) + }, true) + configMapNames.OgcWebserviceProxy = cm.Name + }) + } + + It("Should generate a Deployment correctly", func() { + testMutate("Deployment", getBareDeployment(resource), outputPath+"deployment.yaml", func(d *appsv1.Deployment) error { + return mutateDeployment(reconcilerFn(), resource, d, configMapNames) + }) + }) + + It("Should generate a correct Service", func() { + testMutate("Service", getBareService(resource), outputPath+"service.yaml", func(s *corev1.Service) error { + return mutateService(reconcilerFn(), resource, s) + }) + }) + + It("Should generate a correct Headers Middleware", func() { + testMutate("Headers Middleware", getBareCorsHeadersMiddleware(resource), outputPath+"middleware-headers.yaml", func(m *traefikiov1alpha1.Middleware) error { + return mutateCorsHeadersMiddleware(reconcilerFn(), resource, m) + }) + }) + + It("Should generate a correct IngressRoute", func() { + testMutate("IngressRoute", getBareIngressRoute(resource), outputPath+"ingressroute.yaml", func(i *traefikiov1alpha1.IngressRoute) error { + return mutateIngressRoute(reconcilerFn(), resource, i) + }) + }) + + It("Should generate a correct PodDisruptionBudget", func() { + testMutate("PodDisruptionBudget", getBarePodDisruptionBudget(resource), outputPath+"poddisruptionbudget.yaml", func(p *policyv1.PodDisruptionBudget) error { + return mutatePodDisruptionBudget(reconcilerFn(), resource, p) + }) + }) + + It("Should generate a correct HorizontalPodAutoscaler", func() { + testMutate("PodDisruptionBudget", getBareHorizontalPodAutoScaler(resource), outputPath+"horizontalpodautoscaler.yaml", func(h *v2.HorizontalPodAutoscaler) error { + return mutateHorizontalPodAutoscaler(reconcilerFn(), resource, h) + }) + }) +} + +func readTestFile(fileName string) ([]byte, error) { + dat, err := os.ReadFile(fileName) + if err != nil { + return []byte{}, err + } + + // Temporary check if the input file is a v2, if so, convert to v3 + dat, err = convertAndWriteIfWMSWFS(dat, fileName) + if err != nil { + return []byte{}, err + } + + // Apply defaults + un := unstructured.Unstructured{} + err = yaml.Unmarshal(dat, &un) + if slices.Contains([]string{"WMS", "WFS"}, un.GetKind()) { + defaulted, err := smoothoperatorvalidation.ApplySchemaDefaults(un.Object) + if err != nil { + return []byte{}, err + } + + return yaml.Marshal(defaulted) + } + + return dat, err +} + +func convertAndWriteIfWMSWFS(data []byte, fileName string) ([]byte, error) { + un := unstructured.Unstructured{} + err := yaml.Unmarshal(data, &un) + if err != nil { + return []byte{}, err + } + + if un.GetAPIVersion() == "pdok.nl/v2beta1" { + switch un.GetKind() { + case "WFS": + v2Wfs := v2beta1.WFS{} + err = yaml.UnmarshalStrict(data, &v2Wfs) + if err != nil { + return []byte{}, err + } + v3 := pdoknlv3.WFS{} + err = v2Wfs.ToV3(&v3) + if err != nil { + return []byte{}, err + } + data, err = yaml.Marshal(v3) + case "WMS": + v2Wms := v2beta1.WMS{} + err = yaml.UnmarshalStrict(data, &v2Wms) + if err != nil { + return []byte{}, err + } + v3 := pdoknlv3.WMS{} + err = v2Wms.ToV3(&v3) + if err != nil { + return []byte{}, err + } + data, err = yaml.Marshal(v3) + } + + _ = os.WriteFile(fileName, data, 0644) + } + + return data, err +} + +func TestGetVolumesForDeployment(t *testing.T) { + wfs := &pdoknlv3.WFS{} + data, err := os.ReadFile(testPath("wfs", "minimal") + "input/wfs.yaml") + assert.NoError(t, err) + err = yaml.UnmarshalStrict(data, &wfs) + assert.NoError(t, err) + assert.Equal(t, wfs.Name, "minimal") + pdoknlv3.SetHost("https://service.pdok.nl") + + hashedConfigMapNames := types.HashedConfigMapNames{ + Mapserver: "rws-nwbwegen-v1-0-wfs-mapserver-bb59c7f4f4", + InitScripts: "2", + MapfileGenerator: "rws-nwbwegen-v1-0-wfs-mapfile-generator-bbbtd999dh", + CapabilitiesGenerator: "rws-nwbwegen-v1-0-wfs-capabilities-generator-6m4mfkgb5d", + OgcWebserviceProxy: "3", + LegendGenerator: "4", + FeatureInfoGenerator: "5", + } + result := getVolumes(wfs, hashedConfigMapNames) + + expected := []corev1.Volume{ + {Name: constants.BaseVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, + {Name: constants.DataVolumeName, VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, + {Name: constants.MapserverName, VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: "rws-nwbwegen-v1-0-wfs-mapserver-bb59c7f4f4"}, DefaultMode: smoothoperatorutils.Pointer(int32(420))}}}, + {Name: constants.ConfigMapCapabilitiesGeneratorVolumeName, VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: "rws-nwbwegen-v1-0-wfs-capabilities-generator-6m4mfkgb5d"}, DefaultMode: smoothoperatorutils.Pointer(int32(420))}}}, + {Name: constants.ConfigMapMapfileGeneratorVolumeName, VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: "rws-nwbwegen-v1-0-wfs-mapfile-generator-bbbtd999dh"}, DefaultMode: smoothoperatorutils.Pointer(int32(420))}}}, + } + + assert.Equal(t, expected, result) +} diff --git a/internal/controller/static/files/default_mapserver.conf b/internal/controller/static/files/default_mapserver.conf new file mode 100644 index 0000000..6e17bba --- /dev/null +++ b/internal/controller/static/files/default_mapserver.conf @@ -0,0 +1,8 @@ +CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/config/mapfile/service.map" + END +END \ No newline at end of file diff --git a/internal/controller/static/files/include.conf b/internal/controller/static/files/include.conf new file mode 100644 index 0000000..410d46d --- /dev/null +++ b/internal/controller/static/files/include.conf @@ -0,0 +1,15 @@ +server.modules += ( "mod_status" ) + +$HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { + status.status-url = "/server-status" +} + +url.rewrite-once = ( +{{ rewrite_rules }} +) + +magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) + +setenv.add-environment += ( + "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, +) diff --git a/internal/controller/static/files/ogc.lua b/internal/controller/static/files/ogc.lua new file mode 100644 index 0000000..06e86e4 --- /dev/null +++ b/internal/controller/static/files/ogc.lua @@ -0,0 +1,83 @@ +if lighty.r.req_attr["request.method"] == "GET" then + + -- obtain service type from environment + serviceType = os.getenv('SERVICE_TYPE'):lower() + + path = lighty.r.req_attr["uri.path"] + query = lighty.r.req_attr["uri.query"] + + -- handle legend requests + if serviceType == "wms" then + _, _, file = path:find(".*/legend/(.*)") + if file then + if file:find(".*%.png") then + local legendPath = "/var/www/legend/" .. file + local stat = lighty.stat(legendPath) + if (not stat or not stat.is_file) then + -- don't serve non existing legend file + return 404 + end + lighty.content = { { filename = legendPath } } + lighty.header['Content-Type'] = "image/png" + return 200 + end + + return 404 + end + end + + params = {} + if query then + for k, v in query:gmatch("([^?&=]+)=([^&]+)") do + k = k:lower() + + params[k] = v + end + end + + -- assign service and version default values + version = params['version'] + service = params['service'] + + if not service then + service = serviceType + else + service = service:lower() + end + + if (service == 'wms' and (not version or version ~= '1.1.1')) then + version = '1.3.0' + end + + if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then + version = '2.0.0' + end + + -- serve static content + request = params['request'] + if request then + request = request:lower() + + staticStatus = 200 + staticContentType = 'text/xml; charset=UTF-8' + if request == 'getcapabilities' then + if (service == 'wms' and version == '1.3.0') then + staticFile = '/var/www/config/capabilities_wms_130.xml' + elseif (service == 'wfs' and version == '2.0.0') then + staticFile = '/var/www/config/capabilities_wfs_200.xml' + end + elseif service == 'wfs' and request == 'getfeature' then + startindex = params['startindex'] + if startindex and tonumber(startindex) > 50000 then + staticFile = '/srv/mapserver/config/scraping-error.xml' + staticStatus = 400 + end + end + + if staticFile then + lighty.content = { { filename = staticFile } } + lighty.header['Content-Type'] = staticContentType + return staticStatus + end + end +end \ No newline at end of file diff --git a/internal/controller/static/files/scraping-error.xml b/internal/controller/static/files/scraping-error.xml new file mode 100644 index 0000000..5632f19 --- /dev/null +++ b/internal/controller/static/files/scraping-error.xml @@ -0,0 +1,8 @@ + + + + + It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. + + + diff --git a/internal/controller/static/reader.go b/internal/controller/static/reader.go new file mode 100644 index 0000000..77a3cdb --- /dev/null +++ b/internal/controller/static/reader.go @@ -0,0 +1,26 @@ +package static + +import ( + "embed" + "slices" +) + +//go:embed files +var embeddedFiles embed.FS + +func GetStaticFiles() ([]string, map[string][]byte) { + // Hardcoded order to get the same order as the old ansible operator + orderedNames := []string{"include.conf", "ogc.lua", "default_mapserver.conf", "scraping-error.xml"} + result := map[string][]byte{} + + files, _ := embeddedFiles.ReadDir("files") + for _, f := range files { + content, _ := embeddedFiles.ReadFile("files/" + f.Name()) + result[f.Name()] = content + if !slices.Contains(orderedNames, f.Name()) { + orderedNames = append(orderedNames, f.Name()) + } + } + + return orderedNames, result +} diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 468bc43..717400e 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -1,31 +1,52 @@ /* -Copyright 2025. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package controller +//nolint:revive // Complains about the dot imports import ( "context" + "encoding/json" + "errors" "os" + "os/exec" "path/filepath" "testing" + pdoknlv2beta1 "github.com/pdok/mapserver-operator/api/v2beta1" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" + traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1" + "golang.org/x/tools/go/packages" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" @@ -56,18 +77,43 @@ func TestControllers(t *testing.T) { var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + //nolint:fatcontext ctx, cancel = context.WithCancel(context.TODO()) + scheme := runtime.NewScheme() var err error - err = pdoknlv3.AddToScheme(scheme.Scheme) + err = pdoknlv2beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = pdoknlv3.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = traefikiov1alpha1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = smoothoperatorv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = clientgoscheme.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") + traefikCRDPath := must(getTraefikCRDPath()) + ownerInfoCRDPath := must(getOwnerInfoCRDPath()) testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, + CRDInstallOptions: envtest.CRDInstallOptions{ + Scheme: scheme, + Paths: []string{ + filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wfs.yaml"), + filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wms.yaml"), + traefikCRDPath, + ownerInfoCRDPath, + }, + ErrorIfPathMissing: true, + }, } // Retrieve the first found binary directory to allow running tests from IDEs @@ -80,9 +126,56 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + + // Deploy blob configmap + secret + blobConfig := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "blobs-testtest", + Namespace: metav1.NamespaceDefault, + }, + } + err = k8sClient.Create(ctx, blobConfig) + Expect(err).NotTo(HaveOccurred()) + + blobSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "blobs-testtest", + Namespace: metav1.NamespaceDefault, + }, + } + err = k8sClient.Create(ctx, blobSecret) + Expect(err).NotTo(HaveOccurred()) + + // Deploy postgres configmap + secret + postgresConfig := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "postgres-testtest", + Namespace: metav1.NamespaceDefault, + }, + } + err = k8sClient.Create(ctx, postgresConfig) + Expect(err).NotTo(HaveOccurred()) + + postgresSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "postgres-testtest", + Namespace: metav1.NamespaceDefault, + }, + } + err = k8sClient.Create(ctx, postgresSecret) + Expect(err).NotTo(HaveOccurred()) + + // Load CRD schemas + err = smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wfs.pdok.nl") + Expect(err).NotTo(HaveOccurred()) + err = smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wms.pdok.nl") + Expect(err).NotTo(HaveOccurred()) + + pdoknlv3.SetHost("http://localhost:32788") + SetStorageClassName("test-storage") }) var _ = AfterSuite(func() { @@ -114,3 +207,42 @@ func getFirstFoundEnvTestBinaryDir() string { } return "" } + +func getOwnerInfoCRDPath() (string, error) { + smoothOperatorModule, err := getModule("github.com/pdok/smooth-operator") + if err != nil { + return "", err + } + if smoothOperatorModule.Dir == "" { + return "", errors.New("cannot find path for smooth-operator module") + } + return filepath.Join(smoothOperatorModule.Dir, "config", "crd", "bases", "pdok.nl_ownerinfo.yaml"), nil +} + +func getTraefikCRDPath() (string, error) { + traefikModule, err := getModule("github.com/traefik/traefik/v3") + if err != nil { + return "", err + } + if traefikModule.Dir == "" { + return "", errors.New("cannot find path for traefik module") + } + return filepath.Join(traefikModule.Dir, "integration", "fixtures", "k8s", "01-traefik-crd.yml"), nil +} + +func getModule(name string) (module *packages.Module, err error) { + out, err := exec.Command("go", "list", "-json", "-m", name).Output() + if err != nil { + return + } + module = &packages.Module{} + err = json.Unmarshal(out, module) + return +} + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..b29d959 --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,118 @@ +apiVersion: v1 +data: + input.yaml: |- + global: + additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd + namespace: http://dataset.geonovum.nl + onlineresourceurl: http://localhost + path: /datasetOwner/dataset/theme + prefix: dataset + version: v1_0 + services: + wfs200: + definition: + capabilities: + featuretypelist: + featuretype: + - abstract: feature "1" abstract + defaultcrs: urn:ogc:def:crs:EPSG::28992 + keywords: + - keyword: + - featuretype-1-keyword-1 + - featuretype-1-keyword-2 + metadataurl: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + name: dataset:featuretype-1-name + othercrs: + - urn:ogc:def:crs:EPSG::25831 + - urn:ogc:def:crs:EPSG::25832 + - urn:ogc:def:crs:EPSG::3034 + - urn:ogc:def:crs:EPSG::3035 + - urn:ogc:def:crs:EPSG::3857 + - urn:ogc:def:crs:EPSG::4258 + - urn:ogc:def:crs:EPSG::4326 + title: feature "1" title + - abstract: feature "2" abstract + defaultcrs: urn:ogc:def:crs:EPSG::28992 + keywords: + - keyword: + - featuretype-2-keyword-1 + - featuretype-2-keyword-2 + metadataurl: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + name: dataset:featuretype-2-name + othercrs: + - urn:ogc:def:crs:EPSG::25831 + - urn:ogc:def:crs:EPSG::25832 + - urn:ogc:def:crs:EPSG::3034 + - urn:ogc:def:crs:EPSG::3035 + - urn:ogc:def:crs:EPSG::3857 + - urn:ogc:def:crs:EPSG::4258 + - urn:ogc:def:crs:EPSG::4326 + title: feature "2" title + - abstract: featuretype-3-abstract + defaultcrs: urn:ogc:def:crs:EPSG::28992 + keywords: + - keyword: + - featuretype-3-keyword-1 + - featuretype-3-keyword-2 + metadataurl: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + name: dataset:featuretype-3-name + othercrs: + - urn:ogc:def:crs:EPSG::25831 + - urn:ogc:def:crs:EPSG::25832 + - urn:ogc:def:crs:EPSG::3034 + - urn:ogc:def:crs:EPSG::3035 + - urn:ogc:def:crs:EPSG::3857 + - urn:ogc:def:crs:EPSG::4258 + - urn:ogc:def:crs:EPSG::4326 + title: featuretype-3-title + operationsmetadata: + extendedcapabilities: + extendedcapabilities: + metadataurl: + mediatype: application/vnd.ogc.csw.GetRecordByIdResponse_xml + url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=metameta-meta-meta-meta-metametameta + responselanguage: + language: dut + spatialdatasetidentifier: + code: bronbron-bron-bron-bron-bronbronbron + supportedlanguages: + defaultlanguage: + language: dut + serviceidentification: + abstract: some "Service" abstract + accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + keywords: + keyword: + - service-keyword-1 + - service-keyword-2 + - infoFeatureAccessService + title: some Service title + serviceprovider: + providersite: + href: http://localhost + type: simple + filename: /var/www/config/capabilities_wfs_200.xml + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-capabilities-generator-mfbh8cgh5c + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml new file mode 100644 index 0000000..2a6d546 --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/configmap-init-scripts.yaml @@ -0,0 +1,190 @@ +apiVersion: v1 +data: + gpkg_download.sh: |- + #!/usr/bin/env bash + + set -euo pipefail + + function download_gpkg() { + local gpkg=$1 + local file=$2 + local url=$3 + + if [ -f "$file" ] && [ ! -f "$file".st ]; then + echo msg=\"File already downloaded\" file=\""$file"\" + else + echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" + + # use curl to check if resource exists + # axel blocks on non-existing resources + curl -IfsS "$url" > /dev/null + + echo start "$gpkg" + ret=0 + # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. + axel -n 1 -T 120 -o "$file" "$url" \ + | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ + | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? + + if [ $ret -ne 0 ] + then + echo -e '\n' + # Download failed ($? != 0). + if [ $ret -eq 1 ] + then + # Axel was not able to resume ($? == 1). Remove file and state file. + if [ -f "$file" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file"\" + rm "$file" + fi + if [ -f "$file.st" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file".st\" + rm "$file".st + fi + else + # Download failed with other error ($? > 1). Remove file if state file does not exist. + if [ ! -f "$file.st" ]; then + echo msg=\"Download failed without state file, removing file\" file=\""$file"\" + rm "$file" + fi + fi + + # Retry the download + echo msg=\"Retry file\" file=\""$file"\" + download_gpkg $gpkg $file $url + fi + fi + } + + function download() { + if [ -z "$BLOBS_ENDPOINT" ]; + then + echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; + exit 1; + fi + + local gpkg=$1 + local file=/srv/data/gpkg/$2 + local url=${BLOBS_ENDPOINT}/${gpkg} + + download_gpkg $gpkg $file $url + + # Check Content-length + download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') + file_size=$(wc -c "$file" | awk '{print $1}') + if [ "$download_size" != "$file_size" ] + then + echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + rm_file_and_exit + else + echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + chown 999:999 "$file" + fi + + # Check ogrinfo + echo "Check gpkg with ogrinfo" + if ! ogrinfo -so "$file" + then + echo "ERROR: ogrinfo check on $file failed" + rm_file_and_exit + fi + + # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) + echo "Check if md5 hash value exists in blob storage" + rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" + + # If file contains valid hash, then check it, else skip + hash=$(awk '{ print $1 }' "${file}.md5sum-remote") + if [[ $hash =~ ^[a-f0-9]{32}$ ]] + then + echo "Valid hash value found" + echo "Compare MD5 hash of remote and downloaded gpkg" + if ! (echo "$hash $file" | md5sum --check); then + rm_file_and_exit + fi + else + echo "No hash found for $file in blob storage, skipping checksum." + fi + + echo "done" + } + + function download_all() { + echo msg=\"Starting GeoPackage downloader\" + + local start_time=$(date '+%s') + + # create target location if not exists + mkdir -p /srv/data/gpkg + chown 999:999 /srv/data/gpkg + + download ${BLOBS_GEOPACKAGES_BUCKET}/key/file-1.gpkg file-1.gpkg; + download ${BLOBS_GEOPACKAGES_BUCKET}/key/file-2.gpkg file-2.gpkg; + + echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) + } + + function rm_file_and_exit() { + echo "Removing $file, to ensure a fresh new download is started when script is executed again" + rm -rf "$file" + + if [ -f "$file.st" ]; then + rm "$file".st + fi + + echo "Exiting..." + exit 1 + } + + download_all | awk -W interactive ' + BEGIN { + state="idle"; + } + + { + if ($0 != "") { + if ($1 == "start") { + gpkg=$2; + state="downloading"; + } else if ($1 == "done") { + state="idle"; + } else if (state == "downloading") { + if ($1 == "progress") { + # reduce output to prevent loki from choking on large log volume + if (last_percentage != $2) { + if ($3 == "") { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; + } else { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; + } + } + last_percentage=$2; + } else { + print "msg=\"" $0 "\" gpkg=" gpkg; + } + } else { + print $0; + } + } + } + ' +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-init-scripts-f8k8ffgmgh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml new file mode 100644 index 0000000..1b67321 --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/configmap-mapfile-generator.yaml @@ -0,0 +1,122 @@ +apiVersion: v1 +data: + input.json: |- + { + "service_title": "some Service title", + "service_abstract": "some \"Service\" abstract", + "service_keywords": "service-keyword-1,service-keyword-2,infoFeatureAccessService", + "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "service-extent", + "service_wfs_maxfeatures": "1000", + "service_namespace_prefix": "dataset", + "service_namespace_uri": "http://dataset.geonovum.nl", + "service_onlineresource": "http://localhost", + "service_path": "/datasetOwner/dataset/theme/wfs/v1_0", + "service_metadata_id": "metameta-meta-meta-meta-metametameta", + "dataset_owner": "authority", + "authority_url": "https://authority-url", + "automatic_casing": true, + "data_epsg": "EPSG:28992", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326" + ], + "layers": [ + { + "name": "featuretype-1-name", + "title": "feature \"1\" title", + "abstract": "feature \"1\" abstract", + "keywords": "featuretype-1-keyword-1,featuretype-1-keyword-2", + "layer_extent": "featuretype-1-extent", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "featuretype-1-column-1" + }, + { + "name": "featuretype-1-column-2" + } + ], + "geometry_type": "Point", + "gpkg_path": "/srv/data/gpkg/file-1.gpkg", + "tablename": "featuretype-1" + }, + { + "name": "featuretype-2-name", + "title": "feature \"2\" title", + "abstract": "feature \"2\" abstract", + "keywords": "featuretype-2-keyword-1,featuretype-2-keyword-2", + "layer_extent": "featuretype-2-extent", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "featuretype-2-column-1", + "alias": "ALIAS_featuretype-2-column-1" + }, + { + "name": "featuretype-2-column-2" + } + ], + "geometry_type": "MultiLine", + "gpkg_path": "/srv/data/gpkg/file-2.gpkg", + "tablename": "featuretype-2" + }, + { + "name": "featuretype-3-name", + "title": "featuretype-3-title", + "abstract": "featuretype-3-abstract", + "keywords": "featuretype-3-keyword-1,featuretype-3-keyword-2", + "layer_extent": "featuretype-3-extent", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "tablename": "featuretype-3", + "geometry_type": "MultiLine", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "featuretype-3-column-1" + }, + { + "name": "featuretype-3-column-2" + } + ], + "postgis": true + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapfile-generator-dkmmf7b5hf + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml b/internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..6f04c9c --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/configmap-mapserver.yaml @@ -0,0 +1,143 @@ +apiVersion: v1 +data: + default_mapserver.conf: |- + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/config/mapfile/service.map" + END + END + include.conf: | + server.modules += ( "mod_status" ) + + $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { + status.status-url = "/server-status" + } + + url.rewrite-once = ( + "/datasetOwner/dataset/theme/wfs/v1_0/legend(.*)" => "/legend$1", + "/datasetOwner/dataset/theme/wfs/v1_0(.*)" => "/mapserver$1", + "/other/path/legend(.*)" => "/legend$1", + "/other/path(.*)" => "/mapserver$1" + ) + + magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) + + setenv.add-environment += ( + "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, + ) + ogc.lua: |- + if lighty.r.req_attr["request.method"] == "GET" then + + -- obtain service type from environment + serviceType = os.getenv('SERVICE_TYPE'):lower() + + path = lighty.r.req_attr["uri.path"] + query = lighty.r.req_attr["uri.query"] + + -- handle legend requests + if serviceType == "wms" then + _, _, file = path:find(".*/legend/(.*)") + if file then + if file:find(".*%.png") then + local legendPath = "/var/www/legend/" .. file + local stat = lighty.stat(legendPath) + if (not stat or not stat.is_file) then + -- don't serve non existing legend file + return 404 + end + lighty.content = { { filename = legendPath } } + lighty.header['Content-Type'] = "image/png" + return 200 + end + + return 404 + end + end + + params = {} + if query then + for k, v in query:gmatch("([^?&=]+)=([^&]+)") do + k = k:lower() + + params[k] = v + end + end + + -- assign service and version default values + version = params['version'] + service = params['service'] + + if not service then + service = serviceType + else + service = service:lower() + end + + if (service == 'wms' and (not version or version ~= '1.1.1')) then + version = '1.3.0' + end + + if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then + version = '2.0.0' + end + + -- serve static content + request = params['request'] + if request then + request = request:lower() + + staticStatus = 200 + staticContentType = 'text/xml; charset=UTF-8' + if request == 'getcapabilities' then + if (service == 'wms' and version == '1.3.0') then + staticFile = '/var/www/config/capabilities_wms_130.xml' + elseif (service == 'wfs' and version == '2.0.0') then + staticFile = '/var/www/config/capabilities_wfs_200.xml' + end + elseif service == 'wfs' and request == 'getfeature' then + startindex = params['startindex'] + if startindex and tonumber(startindex) > 50000 then + staticFile = '/srv/mapserver/config/scraping-error.xml' + staticStatus = 400 + end + end + + if staticFile then + lighty.content = { { filename = staticFile } } + lighty.header['Content-Type'] = staticContentType + return staticStatus + end + end + end + scraping-error.xml: | + + + + + It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. + + + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapserver-bfcm4d47kh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/complete/expected/deployment.yaml b/internal/controller/test_data/wfs/complete/expected/deployment.yaml new file mode 100644 index 0000000..8f6d85f --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/deployment.yaml @@ -0,0 +1,270 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: true + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: '9117' + prometheus.io/scrape: true + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + spec: + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + - name: SERVICE_TYPE + value: WFS + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + terminationMessagePolicy: File + terminationMessagePath: /dev/termination-log + lifecycle: + preStop: + exec: + command: + - sleep + - '15' + livenessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + ports: + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?Service=WFS&Request=GetCapabilities'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/html''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + resources: + limits: + cpu: '2' + memory: 500M + requests: + cpu: '1' + memory: 250M + startupProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?Service=WFS&Request=GetCapabilities'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/html''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - args: + - --scrape_uri=http://localhost/server-status?auto + image: test.test/image:test5 + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePolicy: File + terminationMessagePath: /dev/termination-log + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: '0.02' + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + bash /srv/scripts/gpkg_download.sh; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file-1.gpkg;${BLOBS_GEOPACKAGES_BUCKET}/key/file-2.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePolicy: File + terminationMessagePath: /dev/termination-log + resources: + requests: + cpu: '0.15' + limits: + cpu: '1' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - mountPath: /srv/scripts + name: init-scripts + readOnly: true + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:test4 + imagePullPolicy: IfNotPresent + name: capabilities-generator + terminationMessagePolicy: File + terminationMessagePath: /dev/termination-log + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --not-include + - wfs + - /input/input.json + - /srv/data/config/mapfile + command: + - generate-mapfile + envFrom: + - configMapRef: + name: postgres-testtest + - secretRef: + name: postgres-testtest + image: test.test/image:test2 + imagePullPolicy: IfNotPresent + name: mapfile-generator + terminationMessagePolicy: File + terminationMessagePath: /dev/termination-log + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: mapfile-generator-config + readOnly: true + terminationGracePeriodSeconds: 60 + restartPolicy: Always + dnsPolicy: ClusterFirst + volumes: + - ephemeral: + volumeClaimTemplate: + spec: + storageClassName: test-storage + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 11G + name: base + - emptyDir: {} + name: data + - configMap: + name: complete-wfs-mapserver-bfcm4d47kh + defaultMode: 420 + name: mapserver + - configMap: + defaultMode: 511 + name: complete-wfs-init-scripts-f8k8ffgmgh + name: init-scripts + - configMap: + name: complete-wfs-capabilities-generator-mfbh8cgh5c + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: complete-wfs-mapfile-generator-dkmmf7b5hf + defaultMode: 420 + name: mapfile-generator-config diff --git a/internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..614eb9f --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,52 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 600 + type: Percent + value: 10 + - periodSeconds: 600 + type: Pods + value: 1 + selectPolicy: Max + stabilizationWindowSeconds: 3600 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 300 + maxReplicas: 50 + metrics: + - resource: + name: cpu + target: + averageUtilization: 20 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: complete-wfs-mapserver diff --git a/internal/controller/test_data/wfs/complete/expected/ingressroute.yaml b/internal/controller/test_data/wfs/complete/expected/ingressroute.yaml new file mode 100644 index 0000000..6e9192d --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/ingressroute.yaml @@ -0,0 +1,43 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true + annotations: + uptime.pdok.nl/id: fbe1241d4fed04fb85d2135f182427861156f692 + uptime.pdok.nl/name: COMPLETE INSPIRE WFS + uptime.pdok.nl/tags: public-stats,wfs,inspire + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/theme/wfs/v1_0?Service=WFS&Request=GetCapabilities +spec: + routes: + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/theme/wfs/v1_0`) + middlewares: + - name: complete-wfs-mapserver-headers + services: + - kind: Service + name: complete-wfs-mapserver + port: 80 + - kind: Rule + match: Host(`localhost`) && Path(`/other/path`) + middlewares: + - name: complete-wfs-mapserver-headers + services: + - kind: Service + name: complete-wfs-mapserver + port: 80 diff --git a/internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml b/internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml new file mode 100644 index 0000000..46e387c --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/middleware-headers.yaml @@ -0,0 +1,27 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..7a2f186 --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/poddisruptionbudget.yaml @@ -0,0 +1,31 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme diff --git a/internal/controller/test_data/wfs/complete/expected/service.yaml b/internal/controller/test_data/wfs/complete/expected/service.yaml new file mode 100644 index 0000000..6a8ac84 --- /dev/null +++ b/internal/controller/test_data/wfs/complete/expected/service.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme + name: complete-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + type: ClusterIP + sessionAffinity: None + internalTrafficPolicy: Cluster + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: true + service-type: wfs + service-version: v1_0 + theme: theme diff --git a/internal/controller/test_data/wfs/complete/input/ownerinfo.yaml b/internal/controller/test_data/wfs/complete/input/ownerinfo.yaml new file mode 100644 index 0000000..75a212e --- /dev/null +++ b/internal/controller/test_data/wfs/complete/input/ownerinfo.yaml @@ -0,0 +1,23 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wfs: + serviceProvider: + providerName: PDOK diff --git a/internal/controller/test_data/wfs/complete/input/wfs.yaml b/internal/controller/test_data/wfs/complete/input/wfs.yaml new file mode 100644 index 0000000..f9d8a29 --- /dev/null +++ b/internal/controller/test_data/wfs/complete/input/wfs.yaml @@ -0,0 +1,166 @@ +apiVersion: pdok.nl/v3 +kind: WFS +metadata: + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wfs + service-version: v1_0 + theme: theme + name: complete + namespace: default +spec: + ingressRouteUrls: + - url: http://localhost:32788/datasetOwner/dataset/theme/wfs/v1_0 + - url: http://localhost:32788/other/path + healthCheck: + querystring: Service=WFS&Request=GetCapabilities + mimetype: text/html + horizontalPodAutoscalerPatch: + maxReplicas: 50 + metrics: + - resource: + name: cpu + target: + averageUtilization: 20 + type: Utilization + type: Resource + minReplicas: 1 + options: {} + podSpecPatch: + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + - name: mapfile-generator + envFrom: + - configMapRef: + name: postgres-testtest + - secretRef: + name: postgres-testtest + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + resources: + limits: + cpu: "2" + ephemeral-storage: 11G + memory: 500M + requests: + cpu: "1" + ephemeral-storage: 11G + memory: 250M + service: + abstract: some "Service" abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + bbox: + defaultCRS: + maxx: "3" + maxy: "4" + minx: "1" + miny: "2" + defaultCrs: EPSG:28992 + featureTypes: + - abstract: feature "1" abstract + bbox: + defaultCRS: + maxx: "3" + maxy: "4" + minx: "1" + miny: "2" + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file-1.gpkg + columns: + - name: featuretype-1-column-1 + - name: featuretype-1-column-2 + geometryType: Point + tableName: featuretype-1 + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - featuretype-1-keyword-1 + - featuretype-1-keyword-2 + name: featuretype-1-name + title: feature "1" title + - abstract: feature "2" abstract + bbox: + defaultCRS: + maxx: "7" + maxy: "8" + minx: "5" + miny: "6" + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file-2.gpkg + columns: + - alias: ALIAS_featuretype-2-column-1 + name: featuretype-2-column-1 + - name: featuretype-2-column-2 + geometryType: MultiLineString + tableName: featuretype-2 + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - featuretype-2-keyword-1 + - featuretype-2-keyword-2 + name: featuretype-2-name + title: feature "2" title + - abstract: featuretype-3-abstract + bbox: + defaultCRS: + maxx: "9" + maxy: "0" + minx: "1" + miny: "2" + wgs84: + maxx: "180" + maxy: "90" + minx: "-180" + miny: "-90" + data: + postgis: + columns: + - name: featuretype-3-column-1 + - name: featuretype-3-column-2 + geometryType: MultiLineString + tableName: featuretype-3 + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - featuretype-3-keyword-1 + - featuretype-3-keyword-2 + name: featuretype-3-name + title: featuretype-3-title + inspire: + language: dut + serviceMetadataUrl: + csw: + metadataIdentifier: metameta-meta-meta-meta-metametameta + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + keywords: + - service-keyword-1 + - service-keyword-2 + otherCrs: + - EPSG:25831 + - EPSG:25832 + - EPSG:3034 + - EPSG:3035 + - EPSG:3857 + - EPSG:4258 + - EPSG:4326 + ownerInfoRef: owner + prefix: dataset + title: some Service title + url: http://localhost:32788/datasetOwner/dataset/theme/wfs/v1_0 diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..76bb089 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +data: + input.yaml: "..." +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: minimal-wfs-capabilities-generator-m46924mtk7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml new file mode 100644 index 0000000..367a3f0 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/configmap-init-scripts.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +data: + gpkg_download.sh: "..." +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: minimal-wfs-init-scripts-f8k8ffgmgh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml new file mode 100644 index 0000000..9bc7ba5 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/configmap-mapfile-generator.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +data: + input.json: "..." +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapfile-generator-cdchdd74m7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml b/internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..3682d6c --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/configmap-mapserver.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +data: + default_mapserver.conf: "..." + include.conf: "..." + ogc.lua: "..." + scraping-error.xml: "..." +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapserver-f5ch9b2bhh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/minimal/expected/deployment.yaml b/internal/controller/test_data/wfs/minimal/expected/deployment.yaml new file mode 100644 index 0000000..9001517 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/deployment.yaml @@ -0,0 +1,253 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: '9117' + prometheus.io/scrape: 'true' + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + spec: + containers: + - env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + - name: SERVICE_TYPE + value: WFS + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + lifecycle: + preStop: + exec: + command: + - sleep + - '15' + livenessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + name: mapserver + ports: + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + resources: + limits: + ephemeral-storage: 200M + memory: 800M + requests: + cpu: '0.15' + startupProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - args: + - --scrape_uri=http://localhost/server-status?auto + image: test.test/image:test5 + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: '0.02' + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + bash /srv/scripts/gpkg_download.sh; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + requests: + cpu: '0.15' + limits: + cpu: '0.2' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - mountPath: /srv/scripts + name: init-scripts + readOnly: true + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:test4 + imagePullPolicy: IfNotPresent + name: capabilities-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --not-include + - wfs + - /input/input.json + - /srv/data/config/mapfile + command: + - generate-mapfile + image: test.test/image:test2 + imagePullPolicy: IfNotPresent + name: mapfile-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: mapfile-generator-config + readOnly: true + restartPolicy: Always + terminationGracePeriodSeconds: 60 + dnsPolicy: ClusterFirst + volumes: + - emptyDir: {} + name: base + - emptyDir: {} + name: data + - configMap: + name: minimal-wfs-mapserver-f5ch9b2bhh + defaultMode: 420 + name: mapserver + - configMap: + defaultMode: 511 + name: minimal-wfs-init-scripts-f8k8ffgmgh + name: init-scripts + - configMap: + name: minimal-wfs-capabilities-generator-m46924mtk7 + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: minimal-wfs-mapfile-generator-cdchdd74m7 + defaultMode: 420 + name: mapfile-generator-config diff --git a/internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..c35c146 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 600 + type: Percent + value: 10 + - periodSeconds: 600 + type: Pods + value: 1 + selectPolicy: Max + stabilizationWindowSeconds: 3600 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 300 + maxReplicas: 30 + metrics: + - resource: + name: cpu + target: + averageUtilization: 90 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: minimal-wfs-mapserver diff --git a/internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml b/internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml new file mode 100644 index 0000000..7affcb3 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/ingressroute.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapserver + namespace: default + annotations: + uptime.pdok.nl/id: b333bf07fbf71b3ca2e55750b75d8a72309b9564 + uptime.pdok.nl/name: MINIMAL WFS + uptime.pdok.nl/tags: public-stats,wfs + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1 + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + routes: + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/wfs/v1_0`) + middlewares: + - name: minimal-wfs-mapserver-headers + services: + - kind: Service + name: minimal-wfs-mapserver + port: 80 diff --git a/internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml b/internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml new file mode 100644 index 0000000..adab435 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/middleware-headers.yaml @@ -0,0 +1,26 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..58f36f4 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/poddisruptionbudget.yaml @@ -0,0 +1,29 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 diff --git a/internal/controller/test_data/wfs/minimal/expected/service.yaml b/internal/controller/test_data/wfs/minimal/expected/service.yaml new file mode 100644 index 0000000..342df3f --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/expected/service.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: minimal-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + internalTrafficPolicy: Cluster + sessionAffinity: None + type: ClusterIP + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 diff --git a/internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml b/internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml new file mode 100644 index 0000000..75a212e --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/input/ownerinfo.yaml @@ -0,0 +1,23 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wfs: + serviceProvider: + providerName: PDOK diff --git a/internal/controller/test_data/wfs/minimal/input/wfs.yaml b/internal/controller/test_data/wfs/minimal/input/wfs.yaml new file mode 100644 index 0000000..a458f76 --- /dev/null +++ b/internal/controller/test_data/wfs/minimal/input/wfs.yaml @@ -0,0 +1,71 @@ +apiVersion: pdok.nl/v3 +kind: WFS +metadata: + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wfs + service-version: v1_0 + name: minimal + namespace: default +spec: + options: {} + podSpecPatch: + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + resources: + limits: + ephemeral-storage: 100M + service: + abstract: service-abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + bbox: + defaultCRS: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + defaultCrs: EPSG:28992 + featureTypes: + - abstract: featuretype-abstract + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: featuretype-column + geometryType: Point + tableName: featuretype + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - featuretype-keyword + name: featuretype-name + title: featuretype-title + keywords: + - service-keyword + otherCrs: + - EPSG:25831 + - EPSG:25832 + - EPSG:3034 + - EPSG:3035 + - EPSG:3857 + - EPSG:4258 + - EPSG:4326 + ownerInfoRef: owner + prefix: dataset + title: service-title + url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0 diff --git a/internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..2c2561a --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +data: + input.yaml: "..." +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-capabilities-generator-m46924mtk7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml new file mode 100644 index 0000000..1f653ce --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapfile-generator.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +data: + input.json: "..." +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapfile-generator-cdchdd74m7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml b/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..24c9ced --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/configmap-mapserver.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +data: + default_mapserver.conf: "..." + include.conf: "..." + ogc.lua: "..." + scraping-error.xml: "..." +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapserver-f5ch9b2bhh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml b/internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml new file mode 100644 index 0000000..bbf317b --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/deployment.yaml @@ -0,0 +1,245 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: '9117' + prometheus.io/scrape: 'true' + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + spec: + containers: + - env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + - name: SERVICE_TYPE + value: WFS + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + lifecycle: + preStop: + exec: + command: + - sleep + - '15' + livenessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&request=GetCapabilities'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + name: mapserver + ports: + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + resources: + limits: + ephemeral-storage: 200M + memory: 800M + requests: + cpu: '0.15' + startupProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - args: + - --scrape_uri=http://localhost/server-status?auto + image: test.test/image:test5 + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: '0.02' + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + requests: + cpu: '0.15' + limits: + cpu: '0.2' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:test4 + imagePullPolicy: IfNotPresent + name: capabilities-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --not-include + - wfs + - /input/input.json + - /srv/data/config/mapfile + command: + - generate-mapfile + image: test.test/image:test2 + imagePullPolicy: IfNotPresent + name: mapfile-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: mapfile-generator-config + readOnly: true + restartPolicy: Always + terminationGracePeriodSeconds: 60 + dnsPolicy: ClusterFirst + volumes: + - emptyDir: {} + name: base + - emptyDir: {} + name: data + - configMap: + name: noprefetch-wfs-mapserver-f5ch9b2bhh + defaultMode: 420 + name: mapserver + - configMap: + name: noprefetch-wfs-capabilities-generator-m46924mtk7 + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: noprefetch-wfs-mapfile-generator-cdchdd74m7 + defaultMode: 420 + name: mapfile-generator-config diff --git a/internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..1543321 --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 600 + type: Percent + value: 10 + - periodSeconds: 600 + type: Pods + value: 1 + selectPolicy: Max + stabilizationWindowSeconds: 3600 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 300 + maxReplicas: 30 + metrics: + - resource: + name: cpu + target: + averageUtilization: 90 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: noprefetch-wfs-mapserver diff --git a/internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml b/internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml new file mode 100644 index 0000000..7b43d28 --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/ingressroute.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapserver + namespace: default + annotations: + uptime.pdok.nl/id: 7bd64fef831d74baee2ef9158b98f786511e1bc6 + uptime.pdok.nl/name: NOPREFETCH WFS + uptime.pdok.nl/tags: public-stats,wfs + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0?SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature&TYPENAMES=featuretype-name&STARTINDEX=0&COUNT=1 + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + routes: + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/wfs/v1_0`) + middlewares: + - name: noprefetch-wfs-mapserver-headers + services: + - kind: Service + name: noprefetch-wfs-mapserver + port: 80 diff --git a/internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml b/internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml new file mode 100644 index 0000000..058beed --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/middleware-headers.yaml @@ -0,0 +1,26 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..d437eda --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/poddisruptionbudget.yaml @@ -0,0 +1,29 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wfs + service-version: v1_0 diff --git a/internal/controller/test_data/wfs/noprefetch/expected/service.yaml b/internal/controller/test_data/wfs/noprefetch/expected/service.yaml new file mode 100644 index 0000000..7e2675a --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/expected/service.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 + name: noprefetch-wfs-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WFS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + internalTrafficPolicy: Cluster + sessionAffinity: None + type: ClusterIP + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wfs + service-version: v1_0 diff --git a/internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml b/internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml new file mode 100644 index 0000000..75a212e --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/input/ownerinfo.yaml @@ -0,0 +1,23 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wfs: + serviceProvider: + providerName: PDOK diff --git a/internal/controller/test_data/wfs/noprefetch/input/wfs.yaml b/internal/controller/test_data/wfs/noprefetch/input/wfs.yaml new file mode 100644 index 0000000..d65548f --- /dev/null +++ b/internal/controller/test_data/wfs/noprefetch/input/wfs.yaml @@ -0,0 +1,72 @@ +apiVersion: pdok.nl/v3 +kind: WFS +metadata: + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wfs + service-version: v1_0 + name: noprefetch + namespace: default +spec: + options: + prefetchData: false + podSpecPatch: + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + resources: + limits: + ephemeral-storage: 100M + service: + abstract: service-abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + bbox: + defaultCRS: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + defaultCrs: EPSG:28992 + featureTypes: + - abstract: featuretype-abstract + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: featuretype-column + geometryType: Point + tableName: featuretype + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - featuretype-keyword + name: featuretype-name + title: featuretype-title + keywords: + - service-keyword + otherCrs: + - EPSG:25831 + - EPSG:25832 + - EPSG:3034 + - EPSG:3035 + - EPSG:3857 + - EPSG:4258 + - EPSG:4326 + ownerInfoRef: owner + prefix: dataset + title: service-title + url: http://localhost:32788/datasetOwner/dataset/wfs/v1_0 diff --git a/internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..45e7cf9 --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,244 @@ +apiVersion: v1 +data: + input.yaml: |- + global: + additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd + http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd + namespace: http://dataset.geonovum.nl + onlineresourceurl: http://localhost + path: /datasetOwner/dataset/2016 + prefix: dataset + version: v1_0 + services: + wms130: + definition: + capability: + wmscapabilities: + extendedcapabilities: + metadataurl: + mediatype: application/vnd.ogc.csw.GetRecordByIdResponse_xml + url: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=metameta-meta-meta-meta-metametameta + responselanguage: + language: dut + supportedlanguages: + defaultlanguage: + language: dut + layer: + - abstract: Top "Layer" Abstract + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - top-layer-keyword-1 + - top-layer-keyword-2 + layer: + - abstract: group layer abstract "1" + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - group-layer-keyword-1 + - group-layer-keyword-2 + layer: + - abstract: gpkg-layer-abstract "2" + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - gpkg-layer-keyword-1 + - gpkg-layer-keyword-2 + maxscaledenominator: 20 + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + minscaledenominator: 30 + name: gpkg-layer-name + queryable: 1 + style: + - abstract: gpkg-layer-style-1-abstract + legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/gpkg-layer-name/gpkg-layer-style-1-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: gpkg-layer-style-1-name + title: gpkg-layer-style-1-title + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/gpkg-layer-name/gpkg-layer-style-2-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: gpkg-layer-style-2-name + title: gpkg-layer-style-2-title + title: gpkg-layer-title "2" + - abstract: postgis-layer-abstract + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - postgis-layer-keyword-1 + - postgis-layer-keyword-2 + maxscaledenominator: 20 + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + minscaledenominator: 30 + name: postgis-layer-name + queryable: 1 + style: + - abstract: postgis-layer-style-1-abstract + legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/postgis-layer-name/postgis-layer-style-1-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: postgis-layer-style-1-name + title: postgis-layer-style-1-title + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/postgis-layer-name/postgis-layer-style-2-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: postgis-layer-style-2-name + title: postgis-layer-style-2-title + title: postgis-layer-title + maxscaledenominator: 50 + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group-layer-name + queryable: 1 + style: + - abstract: group-layer-style-1-abstract + legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/group-layer-name/group-layer-style-1-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group-layer-style-1-name + title: group-layer-style-1-title + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/group-layer-name/group-layer-style-2-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group-layer-style-2-name + title: group-layer-style-2-title + title: group layer title "1" + maxscaledenominator: 50 + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: top-layer-name + queryable: 1 + style: + - abstract: top-layer-style-1-abstract + legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/top-layer-name/top-layer-style-1-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: top-layer-style-1-name + title: top-layer-style-1-title + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/2016/wms/v1_0/legend/top-layer-name/top-layer-style-2-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: top-layer-style-2-name + title: top-layer-style-2-title + title: Top "Layer" Title + service: + abstract: some "service" abstract + accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + keywordlist: + keyword: + - service-keyword-1 + - service-keyword-2 + - infoMapAccessService + optionalconstraints: + maxheight: 4000 + maxwidth: 4000 + title: some service title + filename: /var/www/config/capabilities_wms_130.xml + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-capabilities-generator-b9kmb96877 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml new file mode 100644 index 0000000..6880d7b --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/configmap-featureinfo-generator.yaml @@ -0,0 +1,72 @@ +apiVersion: v1 +data: + input.json: |- + { + "Projection": "EPSG:28992", + "AutomaticCasing": true, + "Version": 2, + "Layers": [ + { + "Name": "gpkg-layer-name", + "GroupName": "group-layer-name", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "column-1", + "Alias": "ALIAS_column-1" + }, + { + "Name": "column-2" + } + ] + }, + { + "Name": "postgis-layer-name", + "GroupName": "group-layer-name", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "column-1" + }, + { + "Name": "column-2" + } + ] + }, + { + "Name": "tif-layer-name", + "Properties": [ + { + "Name": "value_list" + }, + { + "Name": "class" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-featureinfo-generator-257f6m6228 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml new file mode 100644 index 0000000..031880d --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/configmap-init-scripts.yaml @@ -0,0 +1,190 @@ +--- +apiVersion: v1 +data: + gpkg_download.sh: |- + #!/usr/bin/env bash + + set -euo pipefail + + function download_gpkg() { + local gpkg=$1 + local file=$2 + local url=$3 + + if [ -f "$file" ] && [ ! -f "$file".st ]; then + echo msg=\"File already downloaded\" file=\""$file"\" + else + echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" + + # use curl to check if resource exists + # axel blocks on non-existing resources + curl -IfsS "$url" > /dev/null + + echo start "$gpkg" + ret=0 + # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. + axel -n 1 -T 120 -o "$file" "$url" \ + | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ + | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? + + if [ $ret -ne 0 ] + then + echo -e '\n' + # Download failed ($? != 0). + if [ $ret -eq 1 ] + then + # Axel was not able to resume ($? == 1). Remove file and state file. + if [ -f "$file" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file"\" + rm "$file" + fi + if [ -f "$file.st" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file".st\" + rm "$file".st + fi + else + # Download failed with other error ($? > 1). Remove file if state file does not exist. + if [ ! -f "$file.st" ]; then + echo msg=\"Download failed without state file, removing file\" file=\""$file"\" + rm "$file" + fi + fi + + # Retry the download + echo msg=\"Retry file\" file=\""$file"\" + download_gpkg $gpkg $file $url + fi + fi + } + + function download() { + if [ -z "$BLOBS_ENDPOINT" ]; + then + echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; + exit 1; + fi + + local gpkg=$1 + local file=/srv/data/gpkg/$2 + local url=${BLOBS_ENDPOINT}/${gpkg} + + download_gpkg $gpkg $file $url + + # Check Content-length + download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') + file_size=$(wc -c "$file" | awk '{print $1}') + if [ "$download_size" != "$file_size" ] + then + echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + rm_file_and_exit + else + echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + chown 999:999 "$file" + fi + + # Check ogrinfo + echo "Check gpkg with ogrinfo" + if ! ogrinfo -so "$file" + then + echo "ERROR: ogrinfo check on $file failed" + rm_file_and_exit + fi + + # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) + echo "Check if md5 hash value exists in blob storage" + rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" + + # If file contains valid hash, then check it, else skip + hash=$(awk '{ print $1 }' "${file}.md5sum-remote") + if [[ $hash =~ ^[a-f0-9]{32}$ ]] + then + echo "Valid hash value found" + echo "Compare MD5 hash of remote and downloaded gpkg" + if ! (echo "$hash $file" | md5sum --check); then + rm_file_and_exit + fi + else + echo "No hash found for $file in blob storage, skipping checksum." + fi + + echo "done" + } + + function download_all() { + echo msg=\"Starting GeoPackage downloader\" + + local start_time=$(date '+%s') + + # create target location if not exists + mkdir -p /srv/data/gpkg + chown 999:999 /srv/data/gpkg + + download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; + + echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) + } + + function rm_file_and_exit() { + echo "Removing $file, to ensure a fresh new download is started when script is executed again" + rm -rf "$file" + + if [ -f "$file.st" ]; then + rm "$file".st + fi + + echo "Exiting..." + exit 1 + } + + download_all | awk -W interactive ' + BEGIN { + state="idle"; + } + + { + if ($0 != "") { + if ($1 == "start") { + gpkg=$2; + state="downloading"; + } else if ($1 == "done") { + state="idle"; + } else if (state == "downloading") { + if ($1 == "progress") { + # reduce output to prevent loki from choking on large log volume + if (last_percentage != $2) { + if ($3 == "") { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; + } else { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; + } + } + last_percentage=$2; + } else { + print "msg=\"" $0 "\" gpkg=" gpkg; + } + } else { + print $0; + } + } + } + ' +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: "2016" + name: complete-wms-init-scripts-f8k8ffgmgh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml new file mode 100644 index 0000000..86cb48a --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/configmap-legend-generator.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +data: + default_mapserver.conf: "..." + input: |- + "top-layer-name" "top-layer-style-1-name" + "top-layer-name" "top-layer-style-2-name" + "group-layer-name" "group-layer-style-1-name" + "group-layer-name" "group-layer-style-2-name" + "group-layer-name" "top-layer-style-1-name" + "gpkg-layer-name" "gpkg-layer-style-1-name" + "gpkg-layer-name" "gpkg-layer-style-2-name" + "gpkg-layer-name" "top-layer-style-1-name" + "gpkg-layer-name" "group-layer-style-2-name" + "postgis-layer-name" "postgis-layer-style-1-name" + "postgis-layer-name" "postgis-layer-style-2-name" + legend-fixer.sh: |- + #!/usr/bin/env bash + set -eo pipefail + echo "creating legends for root and group layers by concatenating data layers" + input_filepath="/input/input" + remove_filepath="/input/remove" + config_filepath="/input/ogc-webservice-proxy-config.yaml" + legend_dir="/var/www/legend" + < "${input_filepath}" xargs -n 2 echo | while read -r layer style; do + export layer + # shellcheck disable=SC2016 # dollar is for yq + if ! < "${config_filepath}" yq -e 'env(layer) as $layer | .grouplayers | keys | contains([$layer])' &>/dev/null; then + continue + fi + export grouplayer="${layer}" + grouplayer_style_filepath="${legend_dir}/${grouplayer}/${style}.png" + # shellcheck disable=SC2016 # dollar is for yq + datalayers=$(< "${config_filepath}" yq 'env(grouplayer) as $foo | .grouplayers[$foo][]') + datalayer_style_filepaths=() + for datalayer in $datalayers; do + datalayer_style_filepath="${legend_dir}/${datalayer}/${style}.png" + if [[ -f "${datalayer_style_filepath}" ]]; then + datalayer_style_filepaths+=("${datalayer_style_filepath}") + fi + done + if [[ -n "${datalayer_style_filepaths[*]}" ]]; then + echo "concatenating ${grouplayer_style_filepath}" + gm convert -append "${datalayer_style_filepaths[@]}" "${grouplayer_style_filepath}" + else + echo "no data for ${grouplayer_style_filepath}" + fi + done + < "${remove_filepath}" xargs -n 2 echo | while read -r layer style; do + remove_legend_file="${legend_dir}/${layer}/${style}.png" + echo removing $remove_legend_file + rm $remove_legend_file + done + echo "done" + ogc-webservice-proxy-config.yaml: |- + grouplayers: + group-layer-name: + - gpkg-layer-name + - postgis-layer-name + top-layer-name: + - gpkg-layer-name + - postgis-layer-name + - tif-layer-name + + remove: |- + "group-layer-name" "top-layer-style-1-name" + "gpkg-layer-name" "top-layer-style-1-name" +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-legend-generator-bmg7f9t24k + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml new file mode 100644 index 0000000..bd1092f --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/configmap-mapfile-generator.yaml @@ -0,0 +1,179 @@ +apiVersion: v1 +data: + input.json: |- + { + "service_title": "some service title", + "service_abstract": "some \"service\" abstract", + "service_keywords": "service-keyword-1,service-keyword-2,infoMapAccessService", + "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "service-extent", + "maxSize": "4000", + "service_namespace_prefix": "dataset", + "service_namespace_uri": "http://dataset.geonovum.nl", + "service_onlineresource": "http://localhost", + "service_path": "/datasetOwner/dataset/2016/wms/v1_0", + "service_metadata_id": "metameta-meta-meta-meta-metametameta", + "dataset_owner": "authority-name", + "authority_url": "http://authority-url", + "automatic_casing": true, + "data_epsg": "EPSG:28992", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "templates": "/srv/data/config/templates", + "fonts": "/srv/data/config/fonts", + "top_level_name": "top-layer-name", + "resolution": "20", + "defresolution": "10", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "symbols": [ + "/styling/gpkg-layer-symbol.symbol", + "/styling/tif-layer-symbol.symbol" + ], + "group_layers": [ + { + "name": "group-layer-name", + "title": "group layer title \"1\"", + "abstract": "group layer abstract \"1\"", + "style_name": "group-layer-style-1-name", + "style_title": "group-layer-style-1-title" + } + ], + "layers": [ + { + "name": "gpkg-layer-name", + "group_name": "group-layer-name", + "gpkg_path": "/srv/data/gpkg/file.gpkg", + "tablename": "gpkg-layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "column-1", + "alias": "ALIAS_column-1" + }, + { + "name": "column-2" + } + ], + "title": "gpkg-layer-title \"2\"", + "abstract": "gpkg-layer-abstract \"2\"", + "keywords": "gpkg-layer-keyword-1,gpkg-layer-keyword-2", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "layer_extent": "gpkg-layer-extent", + "minscale": "30", + "maxscale": "20", + "styles": [ + { + "title": "gpkg-layer-style-1-title", + "path": "/styling/gpkg-layer-style-1.style" + }, + { + "title": "gpkg-layer-style-2-title", + "path": "/styling/gpkg-layer-style-2.style" + }, + { + "title": "gpkg-layer-style-3-title", + "path": "/styling/gpkg-layer-style-3.style" + }, + { + "title": "gpkg-layer-style-4-title", + "path": "/styling/gpkg-layer-style-4.style" + } + ] + }, + { + "name": "postgis-layer-name", + "group_name": "group-layer-name", + "tablename": "postgis-layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "column-1" + }, + { + "name": "column-2" + } + ], + "postgis": true, + "title": "postgis-layer-title", + "abstract": "postgis-layer-abstract", + "keywords": "postgis-layer-keyword-1,postgis-layer-keyword-2", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "layer_extent": "postgis-layer-extent", + "minscale": "30", + "maxscale": "20", + "styles": [ + { + "title": "postgis-layer-style-1-title", + "path": "/styling/postgis-layer-style-1.style" + }, + { + "title": "postgis-layer-style-2-title", + "path": "/styling/postgis-layer-style-2.style" + } + ] + }, + { + "name": "tif-layer-name", + "resample": "AVERAGE", + "tif_path": "/srv/data/tif/file.tif", + "geometry_type": "Raster", + "offsite": "#FF00FF", + "get_feature_info_includes_class": true, + "title": "", + "abstract": "", + "keywords": "", + "dataset_metadata_id": "", + "dataset_source_id": "", + "layer_extent": "tif-layer-extent", + "minscale": "30", + "maxscale": "20", + "label_no_clip": true, + "styles": [ + { + "path": "/styling/tif-layer-style-1.style" + }, + { + "path": "/styling/tif-layer-style-2.style" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-mapfile-generator-gh2fg6ccm9 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..d279042 --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/configmap-mapserver.yaml @@ -0,0 +1,147 @@ +--- +apiVersion: v1 +data: + default_mapserver.conf: |- + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/config/mapfile/service.map" + END + END + include.conf: >- + server.modules += ( "mod_status" ) + + + $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { + status.status-url = "/server-status" + } + + + url.rewrite-once = ( + "/datasetOwner/dataset/2016/wms/v1_0/legend(.*)" => "/legend$1", + "/datasetOwner/dataset/2016/wms/v1_0(.*)" => "/mapserver$1" + ) + + + magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) + + + setenv.add-environment += ( + "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, + ) + ogc.lua: >- + if lighty.r.req_attr["request.method"] == "GET" then + + -- obtain service type from environment + serviceType = os.getenv('SERVICE_TYPE'):lower() + + path = lighty.r.req_attr["uri.path"] + query = lighty.r.req_attr["uri.query"] + + -- handle legend requests + if serviceType == "wms" then + _, _, file = path:find(".*/legend/(.*)") + if file then + if file:find(".*%.png") then + local legendPath = "/var/www/legend/" .. file + local stat = lighty.stat(legendPath) + if (not stat or not stat.is_file) then + -- don't serve non existing legend file + return 404 + end + lighty.content = { { filename = legendPath } } + lighty.header['Content-Type'] = "image/png" + return 200 + end + + return 404 + end + end + + params = {} + if query then + for k, v in query:gmatch("([^?&=]+)=([^&]+)") do + k = k:lower() + + params[k] = v + end + end + + -- assign service and version default values + version = params['version'] + service = params['service'] + + if not service then + service = serviceType + else + service = service:lower() + end + + if (service == 'wms' and (not version or version ~= '1.1.1')) then + version = '1.3.0' + end + + if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then + version = '2.0.0' + end + + -- serve static content + request = params['request'] + if request then + request = request:lower() + + staticStatus = 200 + staticContentType = 'text/xml; charset=UTF-8' + if request == 'getcapabilities' then + if (service == 'wms' and version == '1.3.0') then + staticFile = '/var/www/config/capabilities_wms_130.xml' + elseif (service == 'wfs' and version == '2.0.0') then + staticFile = '/var/www/config/capabilities_wfs_200.xml' + end + elseif service == 'wfs' and request == 'getfeature' then + startindex = params['startindex'] + if startindex and tonumber(startindex) > 50000 then + staticFile = '/srv/mapserver/config/scraping-error.xml' + staticStatus = 400 + end + end + + if staticFile then + lighty.content = { { filename = staticFile } } + lighty.header['Content-Type'] = staticContentType + return staticStatus + end + end + end + scraping-error.xml: >- + + + + + + It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. + + + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: "2016" + name: complete-wms-mapserver-88ckd472mk + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml new file mode 100644 index 0000000..06d367d --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/configmap-ogc-webservice-proxy.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +data: + service-config.yaml: |- + grouplayers: + group-layer-name: + - gpkg-layer-name + - postgis-layer-name + top-layer-name: + - gpkg-layer-name + - postgis-layer-name + - tif-layer-name + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-ogc-webservice-proxy-8d98h664bh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/complete/expected/deployment.yaml b/internal/controller/test_data/wms/complete/expected/deployment.yaml new file mode 100644 index 0000000..f2cbbdc --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/deployment.yaml @@ -0,0 +1,404 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: '9117' + prometheus.io/scrape: 'true' + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + spec: + restartPolicy: Always + dnsPolicy: ClusterFirst + containers: + - env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + - name: SERVICE_TYPE + value: WMS + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - sleep + - '15' + livenessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabilities'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: text/xml''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + name: mapserver + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=11,22,33,44&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=gpkg-layer-name&STYLES=&FORMAT=image/png'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: image/png''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + resources: + limits: + cpu: '4' + memory: 100M + requests: + cpu: '2' + memory: 50M + startupProbe: + exec: + command: + - /bin/sh + - -c + - 'wget -SO- -T 10 -t 2 ''http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=11,22,33,44&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=top-layer-name,group-layer-name,gpkg-layer-name,postgis-layer-name,tif-layer-name&STYLES=&FORMAT=image/png'' + 2>&1 | egrep -aiA10 ''HTTP/1.1 200'' | egrep -i ''Content-Type: image/png''' + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - args: + - --scrape_uri=http://localhost/server-status?auto + image: test.test/image:test7 + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: '0.02' + - command: + - /ogc-webservice-proxy + - -h=http://127.0.0.1/ + - -t=wms + - -s=/input/service-config.yaml + - -v + - -r + - -d=15 + image: test.test/image:test6 + imagePullPolicy: IfNotPresent + name: ogc-webservice-proxy + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9111 + resources: + limits: + memory: 200M + requests: + cpu: '0.05' + volumeMounts: + - mountPath: /input + name: ogc-webservice-proxy-config + readOnly: true + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + bash /srv/scripts/gpkg_download.sh; + rclone copyto blobs:/${BLOBS_TIF_BUCKET}/key/file.tif /srv/data/tif/file.tif || exit 1; + rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/tif-symbol.png /srv/data/images/tif-symbol.png || exit 1; + rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/gpkg-symbol.png /srv/data/images/gpkg-symbol.png || exit 1; + rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/symbol.svg /srv/data/images/symbol.svg || exit 1; + rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/font-1.ttf /srv/data/config/fonts/font-1.ttf || exit 1; + echo font-1 font-1.ttf >> /srv/data/config/fonts/fonts.list; + rclone copyto blobs:/${BLOBS_RESOURCES_BUCKET}/key/font-2.ttf /srv/data/config/fonts/font-2.ttf || exit 1; + echo font-2 font-2.ttf >> /srv/data/config/fonts/fonts.list; + echo 'generated fonts.list:'; + cat /srv/data/config/fonts/fonts.list; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + requests: + cpu: '0.15' + limits: + cpu: '1' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - mountPath: /srv/scripts + name: init-scripts + readOnly: true + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:test4 + imagePullPolicy: IfNotPresent + name: capabilities-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --not-include + - wms + - /input/input.json + - /srv/data/config/mapfile + command: + - generate-mapfile + envFrom: + - configMapRef: + name: postgres-testtest + - secretRef: + name: postgres-testtest + image: test.test/image:test2 + imagePullPolicy: IfNotPresent + name: mapfile-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: mapfile-generator-config + readOnly: true + - mountPath: /styling + name: styling-files + readOnly: true + - args: + - --input-path + - /input/input.json + - --dest-folder + - /srv/data/config/templates + - --file-name + - feature-info + command: + - featureinfo-generator + image: test.test/image:test5 + imagePullPolicy: IfNotPresent + name: featureinfo-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: featureinfo-generator-config + readOnly: true + - command: + - bash + - -c + - | + set -eu; + exit_code=0; + cat /input/input | xargs -n 2 echo | while read layer style; do + echo Generating legend for layer: $layer, style: $style; + mkdir -p /var/www/legend/$layer; + mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; + magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); + if [[ $magic_bytes != 'PNG' ]]; then + echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; + exit_code=1; + fi; + done; + exit $exit_code; + env: + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + name: legend-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /input + name: legend-generator-config + readOnly: true + - command: + - /bin/bash + - /input/legend-fixer.sh + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: legend-fixer + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: legend-generator-config + readOnly: true + terminationGracePeriodSeconds: 60 + volumes: + - ephemeral: + volumeClaimTemplate: + spec: + accessModes: + - ReadWriteOnce + storageClassName: test-storage + resources: + requests: + storage: 11G + name: base + - emptyDir: {} + name: data + - configMap: + name: complete-wms-mapserver-88ckd472mk + defaultMode: 420 + name: mapserver + - configMap: + name: complete-wms-ogc-webservice-proxy-8d98h664bh + defaultMode: 420 + name: ogc-webservice-proxy-config + - configMap: + defaultMode: 511 + name: complete-wms-init-scripts-f8k8ffgmgh + name: init-scripts + - configMap: + name: complete-wms-capabilities-generator-b9kmb96877 + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: complete-wms-mapfile-generator-gh2fg6ccm9 + defaultMode: 420 + name: mapfile-generator-config + - name: styling-files + projected: + sources: + - configMap: + name: gpkg-styling + - configMap: + name: tif-styling + - configMap: + name: postgis-styling + - configMap: + name: complete-wms-featureinfo-generator-257f6m6228 + defaultMode: 420 + name: featureinfo-generator-config + - configMap: + name: complete-wms-legend-generator-bmg7f9t24k + defaultMode: 420 + name: legend-generator-config diff --git a/internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..bfd17e6 --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: "2016" + name: complete-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 600 + type: Percent + value: 10 + - periodSeconds: 600 + type: Pods + value: 1 + selectPolicy: Max + stabilizationWindowSeconds: 3600 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 0 + maxReplicas: 50 + metrics: + - resource: + name: cpu + target: + averageUtilization: 20 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: complete-wms-mapserver diff --git a/internal/controller/test_data/wms/complete/expected/ingressroute.yaml b/internal/controller/test_data/wms/complete/expected/ingressroute.yaml new file mode 100644 index 0000000..ab4ed9b --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/ingressroute.yaml @@ -0,0 +1,62 @@ +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: "2016" + name: complete-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true + annotations: + uptime.pdok.nl/id: 5b67e76cef85f33507d2ff00ddd73fe85d4eb449 + uptime.pdok.nl/name: COMPLETE INSPIRE WMS + uptime.pdok.nl/tags: public-stats,wms,inspire + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/2016/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=11,22,33,44&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=gpkg-layer-name&STYLES=&FORMAT=image/png +spec: + routes: + - kind: Rule + match: Host(`localhost`) && + PathPrefix(`/datasetOwner/dataset/2016/wms/v1_0/legend`) + middlewares: + - name: complete-wms-mapserver-headers + services: + - kind: Service + name: complete-wms-mapserver + port: 80 + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/2016/wms/v1_0`) + middlewares: + - name: complete-wms-mapserver-headers + services: + - kind: Service + name: complete-wms-mapserver + port: 9111 + - kind: Rule + match: Host(`localhost`) && + PathPrefix(`/other/path/legend`) + middlewares: + - name: complete-wms-mapserver-headers + services: + - kind: Service + name: complete-wms-mapserver + port: 80 + - kind: Rule + match: Host(`localhost`) && Path(`/other/path`) + middlewares: + - name: complete-wms-mapserver-headers + services: + - kind: Service + name: complete-wms-mapserver + port: 9111 diff --git a/internal/controller/test_data/wms/complete/expected/middleware-headers.yaml b/internal/controller/test_data/wms/complete/expected/middleware-headers.yaml new file mode 100644 index 0000000..1587754 --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/middleware-headers.yaml @@ -0,0 +1,27 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..babbc52 --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/poddisruptionbudget.yaml @@ -0,0 +1,31 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' + name: complete-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: '2016' diff --git a/internal/controller/test_data/wms/complete/expected/service.yaml b/internal/controller/test_data/wms/complete/expected/service.yaml new file mode 100644 index 0000000..9aa1fcf --- /dev/null +++ b/internal/controller/test_data/wms/complete/expected/service.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: "2016" + name: complete-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: complete + uid: "" + blockOwnerDeletion: true + controller: true +spec: + type: ClusterIP + sessionAffinity: None + internalTrafficPolicy: Cluster + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: ogc-webservice-proxy + port: 9111 + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "true" + service-type: wms + service-version: v1_0 + theme: "2016" diff --git a/internal/controller/test_data/wms/complete/input/ownerinfo.yaml b/internal/controller/test_data/wms/complete/input/ownerinfo.yaml new file mode 100644 index 0000000..8025b4a --- /dev/null +++ b/internal/controller/test_data/wms/complete/input/ownerinfo.yaml @@ -0,0 +1,36 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wms: + contactInformation: + contactPersonPrimary: + contactPerson: KlantContactCenter PDOK + contactOrganization: PDOK + contactPosition: pointOfContact + contactAddress: + addressType: + address: + city: Apeldoorn + stateOrProvince: + postCode: + country: Netherlands + contactVoiceTelephone: + contactFacsimileTelephone: + contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/complete/input/wms.yaml b/internal/controller/test_data/wms/complete/input/wms.yaml new file mode 100644 index 0000000..142eaa2 --- /dev/null +++ b/internal/controller/test_data/wms/complete/input/wms.yaml @@ -0,0 +1,308 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + creationTimestamp: "2022-09-01T12:00:00Z" + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wms + service-version: v1_0 + theme: 2016 + name: complete + namespace: default +spec: + ingressRouteUrls: + - url: http://localhost:32788/datasetOwner/dataset/2016/wms/v1_0 + - url: http://localhost:32788/other/path + healthCheck: + boundingbox: + maxx: "33" + maxy: "44" + minx: "11" + miny: "22" + horizontalPodAutoscalerPatch: + maxReplicas: 50 + metrics: + - resource: + name: cpu + target: + averageUtilization: 20 + type: Utilization + type: Resource + minReplicas: 1 + lifecycle: + ttlInDays: 730000 + options: + rewriteGroupToDataLayers: true + podSpecPatch: + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + - name: mapfile-generator + envFrom: + - configMapRef: + name: postgres-testtest + - secretRef: + name: postgres-testtest + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + resources: + limits: + cpu: "4" + ephemeral-storage: 11G + memory: 100M + requests: + cpu: "2" + ephemeral-storage: 11G + memory: 50M + service: + prefix: dataset + abstract: some "service" abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + defResolution: 10 + inspire: + language: dut + serviceMetadataUrl: + csw: + metadataIdentifier: metameta-meta-meta-meta-metametameta + keywords: + - service-keyword-1 + - service-keyword-2 + layer: + abstract: Top "Layer" Abstract + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + boundingBoxes: + - bbox: + maxx: "3" + maxy: "4" + minx: "1" + miny: "2" + crs: EPSG:28992 + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - top-layer-keyword-1 + - top-layer-keyword-2 + layers: + - abstract: group layer abstract "1" + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + boundingBoxes: + - bbox: + maxx: "3" + maxy: "4" + minx: "1" + miny: "2" + crs: EPSG:28992 + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - group-layer-keyword-1 + - group-layer-keyword-2 + layers: + - abstract: gpkg-layer-abstract "2" + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + boundingBoxes: + - bbox: + maxx: "7" + maxy: "8" + minx: "5" + miny: "6" + crs: EPSG:28992 + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - alias: ALIAS_column-1 + name: column-1 + - name: column-2 + geometryType: Point + tableName: gpkg-layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - gpkg-layer-keyword-1 + - gpkg-layer-keyword-2 + maxscaledenominator: "20" + minscaledenominator: "30" + name: gpkg-layer-name + styles: + - abstract: gpkg-layer-style-1-abstract + name: gpkg-layer-style-1-name + title: gpkg-layer-style-1-title + visualization: gpkg-layer-style-1.style + - name: gpkg-layer-style-2-name + title: gpkg-layer-style-2-title + visualization: gpkg-layer-style-2.style + - abstract: gpkg-layer-style-3-abstract + name: top-layer-style-1-name + title: gpkg-layer-style-3-title + visualization: gpkg-layer-style-3.style + - abstract: gpkg-layer-style-4-abstract + name: group-layer-style-2-name + title: gpkg-layer-style-4-title + visualization: gpkg-layer-style-4.style + - name: top-layer-style-2-name + visualization: top-layer-style-2.style + - name: group-layer-style-1-name + visualization: group-layer-style-1.style + - name: group-layer-style-3-name + visualization: group-layer-style-3.style + title: gpkg-layer-title "2" + visible: true + - abstract: postgis-layer-abstract + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + boundingBoxes: + - bbox: + maxx: "5" + maxy: "7" + minx: "1" + miny: "3" + crs: EPSG:28992 + data: + postgis: + columns: + - name: column-1 + - name: column-2 + geometryType: Point + tableName: postgis-layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - postgis-layer-keyword-1 + - postgis-layer-keyword-2 + maxscaledenominator: "20" + minscaledenominator: "30" + name: postgis-layer-name + styles: + - abstract: postgis-layer-style-1-abstract + name: postgis-layer-style-1-name + title: postgis-layer-style-1-title + visualization: postgis-layer-style-1.style + - name: postgis-layer-style-2-name + title: postgis-layer-style-2-title + visualization: postgis-layer-style-2.style + - name: top-layer-style-1-name + visualization: top-layer-style-1.style + - name: top-layer-style-2-name + visualization: top-layer-style-2.style + - name: group-layer-style-1-name + visualization: group-layer-style-1.style + - name: group-layer-style-2-name + visualization: group-layer-style-1.style + - name: group-layer-style-3-name + visualization: group-layer-style-1.style + title: postgis-layer-title + visible: true + maxscaledenominator: "50" + name: group-layer-name + styles: + - abstract: group-layer-style-1-abstract + name: group-layer-style-1-name + title: group-layer-style-1-title + - name: group-layer-style-2-name + title: group-layer-style-2-title + - abstract: group-layer-style-3-abstract + name: group-layer-style-3-name + title: group-layer-style-3-title + title: group layer title "1" + visible: true + - boundingBoxes: + - bbox: + maxx: "6" + maxy: "8" + minx: "2" + miny: "4" + crs: EPSG:28992 + keywords: + - keyword + title: title + abstract: abstract + data: + tif: + blobKey: ${BLOBS_TIF_BUCKET}/key/file.tif + getFeatureInfoIncludesClass: true + offsite: '#FF00FF' + resample: AVERAGE + labelNoClip: true + maxscaledenominator: "20" + minscaledenominator: "30" + name: tif-layer-name + styles: + - name: tif-layer-style-1-name + visualization: tif-layer-style-1.style + - name: tif-layer-style-2-name + visualization: tif-layer-style-2.style + - name: top-layer-style-1-name + visualization: top-layer-style-1.style + - name: top-layer-style-2-name + visualization: top-layer-style-2.style + visible: false + maxscaledenominator: "50" + name: top-layer-name + styles: + - abstract: top-layer-style-1-abstract + name: top-layer-style-1-name + title: top-layer-style-1-title + - name: top-layer-style-2-name + title: top-layer-style-2-title + title: Top "Layer" Title + visible: true + ownerInfoRef: owner + resolution: 20 + stylingAssets: + blobKeys: + - ${BLOBS_RESOURCES_BUCKET}/key/tif-symbol.png + - ${BLOBS_RESOURCES_BUCKET}/key/gpkg-symbol.png + - ${BLOBS_RESOURCES_BUCKET}/key/symbol.svg + - ${BLOBS_RESOURCES_BUCKET}/key/font-1.ttf + - ${BLOBS_RESOURCES_BUCKET}/key/font-2.ttf + configMapRefs: + - keys: + - gpkg-layer-style-1.style + - gpkg-layer-style-2.style + - gpkg-layer-style-3.style + - gpkg-layer-style-4.style + - gpkg-layer-symbol.symbol + - top-layer-style-1.style + - top-layer-style-2.style + - group-layer-style-1.style + - group-layer-style-3.style + name: gpkg-styling + - keys: + - tif-layer-style-1.style + - tif-layer-style-2.style + - tif-layer-symbol.symbol + name: tif-styling + - keys: + - postgis-layer-style-1.style + - postgis-layer-style-2.style + name: postgis-styling + title: some service title + url: http://localhost:32788/datasetOwner/dataset/2016/wms/v1_0 diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..109de65 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,150 @@ +--- +apiVersion: v1 +data: + input.yaml: >- + global: + additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd + http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd + namespace: http://dataset.geonovum.nl + onlineresourceurl: http://localhost + path: /datasetOwner/dataset + prefix: dataset + version: v1_0 + services: + wms130: + definition: + capability: + wmscapabilities: + layer: + - abstract: service-abstract + keywordlist: + keyword: + - service-keyword + layer: + - abstract: layer-abstract + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - layer-keyword + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: layer-name + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: layer-style-name + title: layer-style-title + title: layer-title + - abstract: group + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + layer: + - abstract: group-child + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group-child + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group-child + title: group-child + title: group-child + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group + title: group + title: group + queryable: 1 + title: service-title + service: + abstract: service-abstract + accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + keywordlist: + keyword: + - service-keyword + optionalconstraints: + maxheight: 4000 + maxwidth: 4000 + title: service-title + filename: /var/www/config/capabilities_wms_130.xml +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-capabilities-generator-865bt77thd + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml new file mode 100644 index 0000000..c52c3e1 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-featureinfo-generator.yaml @@ -0,0 +1,52 @@ +apiVersion: v1 +data: + input.json: |- + { + "Projection": "EPSG:28992", + "AutomaticCasing": false, + "Version": 2, + "Layers": [ + { + "Name": "layer-name", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + }, + { + "Name": "group-child", + "GroupName": "group", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-featureinfo-generator-668mmh48cc + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml new file mode 100644 index 0000000..7a7f58d --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-init-scripts.yaml @@ -0,0 +1,189 @@ +--- +apiVersion: v1 +data: + gpkg_download.sh: |- + #!/usr/bin/env bash + + set -euo pipefail + + function download_gpkg() { + local gpkg=$1 + local file=$2 + local url=$3 + + if [ -f "$file" ] && [ ! -f "$file".st ]; then + echo msg=\"File already downloaded\" file=\""$file"\" + else + echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" + + # use curl to check if resource exists + # axel blocks on non-existing resources + curl -IfsS "$url" > /dev/null + + echo start "$gpkg" + ret=0 + # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. + axel -n 1 -T 120 -o "$file" "$url" \ + | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ + | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? + + if [ $ret -ne 0 ] + then + echo -e '\n' + # Download failed ($? != 0). + if [ $ret -eq 1 ] + then + # Axel was not able to resume ($? == 1). Remove file and state file. + if [ -f "$file" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file"\" + rm "$file" + fi + if [ -f "$file.st" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file".st\" + rm "$file".st + fi + else + # Download failed with other error ($? > 1). Remove file if state file does not exist. + if [ ! -f "$file.st" ]; then + echo msg=\"Download failed without state file, removing file\" file=\""$file"\" + rm "$file" + fi + fi + + # Retry the download + echo msg=\"Retry file\" file=\""$file"\" + download_gpkg $gpkg $file $url + fi + fi + } + + function download() { + if [ -z "$BLOBS_ENDPOINT" ]; + then + echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; + exit 1; + fi + + local gpkg=$1 + local file=/srv/data/gpkg/$2 + local url=${BLOBS_ENDPOINT}/${gpkg} + + download_gpkg $gpkg $file $url + + # Check Content-length + download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') + file_size=$(wc -c "$file" | awk '{print $1}') + if [ "$download_size" != "$file_size" ] + then + echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + rm_file_and_exit + else + echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + chown 999:999 "$file" + fi + + # Check ogrinfo + echo "Check gpkg with ogrinfo" + if ! ogrinfo -so "$file" + then + echo "ERROR: ogrinfo check on $file failed" + rm_file_and_exit + fi + + # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) + echo "Check if md5 hash value exists in blob storage" + rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" + + # If file contains valid hash, then check it, else skip + hash=$(awk '{ print $1 }' "${file}.md5sum-remote") + if [[ $hash =~ ^[a-f0-9]{32}$ ]] + then + echo "Valid hash value found" + echo "Compare MD5 hash of remote and downloaded gpkg" + if ! (echo "$hash $file" | md5sum --check); then + rm_file_and_exit + fi + else + echo "No hash found for $file in blob storage, skipping checksum." + fi + + echo "done" + } + + function download_all() { + echo msg=\"Starting GeoPackage downloader\" + + local start_time=$(date '+%s') + + # create target location if not exists + mkdir -p /srv/data/gpkg + chown 999:999 /srv/data/gpkg + + download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; + + echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) + } + + function rm_file_and_exit() { + echo "Removing $file, to ensure a fresh new download is started when script is executed again" + rm -rf "$file" + + if [ -f "$file.st" ]; then + rm "$file".st + fi + + echo "Exiting..." + exit 1 + } + + download_all | awk -W interactive ' + BEGIN { + state="idle"; + } + + { + if ($0 != "") { + if ($1 == "start") { + gpkg=$2; + state="downloading"; + } else if ($1 == "done") { + state="idle"; + } else if (state == "downloading") { + if ($1 == "progress") { + # reduce output to prevent loki from choking on large log volume + if (last_percentage != $2) { + if ($3 == "") { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; + } else { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; + } + } + last_percentage=$2; + } else { + print "msg=\"" $0 "\" gpkg=" gpkg; + } + } else { + print $0; + } + } + } + ' +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-init-scripts-f8k8ffgmgh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml new file mode 100644 index 0000000..2530012 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-legend-generator.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +data: + default_mapserver.conf: ... + input: |- + "layer-name" "layer-style-name" + "group" "group" + "group-child" "group-child" +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-legend-generator-82hh8mg962 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..cf06f96 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-mapserver.yaml @@ -0,0 +1,146 @@ +--- +apiVersion: v1 +data: + default_mapserver.conf: |- + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/config/mapfile/service.map" + END + END + include.conf: >- + server.modules += ( "mod_status" ) + + + $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { + status.status-url = "/server-status" + } + + + url.rewrite-once = ( + "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", + "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" + ) + + + magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) + + + setenv.add-environment += ( + "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, + ) + ogc.lua: >- + if lighty.r.req_attr["request.method"] == "GET" then + + -- obtain service type from environment + serviceType = os.getenv('SERVICE_TYPE'):lower() + + path = lighty.r.req_attr["uri.path"] + query = lighty.r.req_attr["uri.query"] + + -- handle legend requests + if serviceType == "wms" then + _, _, file = path:find(".*/legend/(.*)") + if file then + if file:find(".*%.png") then + local legendPath = "/var/www/legend/" .. file + local stat = lighty.stat(legendPath) + if (not stat or not stat.is_file) then + -- don't serve non existing legend file + return 404 + end + lighty.content = { { filename = legendPath } } + lighty.header['Content-Type'] = "image/png" + return 200 + end + + return 404 + end + end + + params = {} + if query then + for k, v in query:gmatch("([^?&=]+)=([^&]+)") do + k = k:lower() + + params[k] = v + end + end + + -- assign service and version default values + version = params['version'] + service = params['service'] + + if not service then + service = serviceType + else + service = service:lower() + end + + if (service == 'wms' and (not version or version ~= '1.1.1')) then + version = '1.3.0' + end + + if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then + version = '2.0.0' + end + + -- serve static content + request = params['request'] + if request then + request = request:lower() + + staticStatus = 200 + staticContentType = 'text/xml; charset=UTF-8' + if request == 'getcapabilities' then + if (service == 'wms' and version == '1.3.0') then + staticFile = '/var/www/config/capabilities_wms_130.xml' + elseif (service == 'wfs' and version == '2.0.0') then + staticFile = '/var/www/config/capabilities_wfs_200.xml' + end + elseif service == 'wfs' and request == 'getfeature' then + startindex = params['startindex'] + if startindex and tonumber(startindex) > 50000 then + staticFile = '/srv/mapserver/config/scraping-error.xml' + staticStatus = 400 + end + end + + if staticFile then + lighty.content = { { filename = staticFile } } + lighty.header['Content-Type'] = staticContentType + return staticStatus + end + end + end + scraping-error.xml: >- + + + + + + It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. + + + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-mapserver-df94mb2d76 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml new file mode 100644 index 0000000..a4389cf --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/configmap-ogc-webservice-proxy.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +data: + service-config.yaml: |- + grouplayers: + group-layer-name: + - gpkg-layer-name + - postgis-layer-name + top-layer-name: + - gpkg-layer-name + - postgis-layer-name + - tif-layer-name + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-ogc-webservice-proxy-22tb5878f7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml new file mode 100644 index 0000000..70a103e --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/deployment.yaml @@ -0,0 +1,341 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: "9117" + prometheus.io/scrape: "true" + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + spec: + restartPolicy: Always + dnsPolicy: ClusterFirst + containers: + - env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + - name: SERVICE_TYPE + value: WMS + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/mapfile.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - sleep + - "15" + livenessProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabil\ + ities' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: text/xml'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + name: mapserver + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png' + 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: image/png'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + resources: + limits: + ephemeral-storage: 200M + memory: 800M + requests: + cpu: "0.1" + startupProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name,group,group-child&STYLES=&FORMAT=image/png' + 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: image/png'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - name: mapfile + mountPath: /srv/data/config/mapfile + - args: + - --scrape_uri=http://localhost/server-status?auto + image: test.test/image:test7 + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: "0.02" + - name: ogc-webservice-proxy + image: test.test/image:test6 + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + limits: + memory: 200M + requests: + cpu: "0.05" + command: + - /ogc-webservice-proxy + - -h=http://127.0.0.1/ + - -t=wms + - -s=/input/service-config.yaml + - -v + - -d=15 + ports: + - containerPort: 9111 + volumeMounts: + - name: ogc-webservice-proxy-config + mountPath: /input + readOnly: true + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + bash /srv/scripts/gpkg_download.sh; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + requests: + cpu: '0.15' + limits: + cpu: '0.2' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - mountPath: /srv/scripts + name: init-scripts + readOnly: true + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:test4 + imagePullPolicy: IfNotPresent + name: capabilities-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --input-path + - /input/input.json + - --dest-folder + - /srv/data/config/templates + - --file-name + - feature-info + command: + - featureinfo-generator + image: test.test/image:test5 + imagePullPolicy: IfNotPresent + name: featureinfo-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: featureinfo-generator-config + readOnly: true + - command: + - bash + - -c + - | + set -eu; + exit_code=0; + cat /input/input | xargs -n 2 echo | while read layer style; do + echo Generating legend for layer: $layer, style: $style; + mkdir -p /var/www/legend/$layer; + mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; + magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); + if [[ $magic_bytes != 'PNG' ]]; then + echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; + exit_code=1; + fi; + done; + exit $exit_code; + env: + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/mapfile.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + name: legend-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/default_mapserver.conf + name: mapserver + subPath: default_mapserver.conf + - name: mapfile + mountPath: /srv/data/config/mapfile + - mountPath: /input + name: legend-generator-config + readOnly: true + terminationGracePeriodSeconds: 60 + volumes: + - emptyDir: {} + name: base + - emptyDir: {} + name: data + - configMap: + name: custom-mapfile-wms-mapserver-df94mb2d76 + defaultMode: 420 + name: mapserver + - configMap: + name: configMap + defaultMode: 420 + name: mapfile + - configMap: + name: custom-mapfile-wms-ogc-webservice-proxy-22tb5878f7 + defaultMode: 420 + name: ogc-webservice-proxy-config + - configMap: + defaultMode: 511 + name: custom-mapfile-wms-init-scripts-f8k8ffgmgh + name: init-scripts + - configMap: + name: custom-mapfile-wms-capabilities-generator-865bt77thd + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: custom-mapfile-wms-featureinfo-generator-668mmh48cc + defaultMode: 420 + name: featureinfo-generator-config + - configMap: + name: custom-mapfile-wms-legend-generator-82hh8mg962 + defaultMode: 420 + name: legend-generator-config diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..10ebb09 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 600 + type: Percent + value: 10 + - periodSeconds: 600 + type: Pods + value: 1 + selectPolicy: Max + stabilizationWindowSeconds: 3600 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 0 + maxReplicas: 30 + metrics: + - resource: + name: cpu + target: + averageUtilization: 90 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: custom-mapfile-wms-mapserver diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml new file mode 100644 index 0000000..d173bd8 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/ingressroute.yaml @@ -0,0 +1,43 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true + annotations: + "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" + uptime.pdok.nl/id: 327614531e386400ce221d6b9fc6d93dc252f0d3 + uptime.pdok.nl/name: CUSTOM mapfile WMS + uptime.pdok.nl/tags: public-stats,wms + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png +spec: + routes: + - kind: Rule + match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) + middlewares: + - name: custom-mapfile-wms-mapserver-headers + services: + - kind: Service + name: custom-mapfile-wms-mapserver + port: 80 + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) + middlewares: + - name: custom-mapfile-wms-mapserver-headers + services: + - kind: Service + name: custom-mapfile-wms-mapserver + port: 9111 diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml new file mode 100644 index 0000000..8537578 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/middleware-headers.yaml @@ -0,0 +1,26 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..ee088dc --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/poddisruptionbudget.yaml @@ -0,0 +1,29 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/custom-mapfile/expected/service.yaml b/internal/controller/test_data/wms/custom-mapfile/expected/service.yaml new file mode 100644 index 0000000..2afea5a --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/expected/service.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: custom-mapfile-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: custom-mapfile + uid: "" + blockOwnerDeletion: true + controller: true +spec: + type: ClusterIP + internalTrafficPolicy: Cluster + sessionAffinity: None + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: ogc-webservice-proxy + port: 9111 + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml b/internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml new file mode 100644 index 0000000..8025b4a --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/input/ownerinfo.yaml @@ -0,0 +1,36 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wms: + contactInformation: + contactPersonPrimary: + contactPerson: KlantContactCenter PDOK + contactOrganization: PDOK + contactPosition: pointOfContact + contactAddress: + addressType: + address: + city: Apeldoorn + stateOrProvince: + postCode: + country: Netherlands + contactVoiceTelephone: + contactFacsimileTelephone: + contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/custom-mapfile/input/wms.yaml b/internal/controller/test_data/wms/custom-mapfile/input/wms.yaml new file mode 100644 index 0000000..5e933c7 --- /dev/null +++ b/internal/controller/test_data/wms/custom-mapfile/input/wms.yaml @@ -0,0 +1,126 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wms + service-version: v1_0 + name: custom-mapfile + namespace: default +spec: + options: {} + podSpecPatch: + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + resources: + limits: + ephemeral-storage: 100m + service: + prefix: dataset + abstract: service-abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + keywords: + - service-keyword + layer: + abstract: service-abstract + keywords: + - service-keyword + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + layers: + - abstract: layer-abstract + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: layer-name + styles: + - name: layer-style-name + title: layer-style-title + title: layer-title + visible: true + - abstract: group + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + layers: + - abstract: group-child + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: group-child + styles: + - name: group-child + title: group-child + - name: style + title: style + title: group-child + visible: true + keywords: + - layer-keyword + name: group + title: group + visible: true + styles: + - name: style + title: style + title: service-title + visible: true + mapfile: + configMapKeyRef: + name: configMap + key: mapfile.map + ownerInfoRef: owner + title: service-title + url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..bfb9ced --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,150 @@ +--- +apiVersion: v1 +data: + input.yaml: >- + global: + additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd + http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd + namespace: http://dataset.geonovum.nl + onlineresourceurl: http://localhost + path: /datasetOwner/dataset + prefix: dataset + version: v1_0 + services: + wms130: + definition: + capability: + wmscapabilities: + layer: + - abstract: service-abstract + keywordlist: + keyword: + - service-keyword + layer: + - abstract: layer-abstract + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - layer-keyword + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: layer-name + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: layer-style-name + title: layer-style-title + title: layer-title + - abstract: group + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + layer: + - abstract: group-child + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group-child + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group-child + title: group-child + title: group-child + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group + title: group + title: group + queryable: 1 + title: service-title + service: + abstract: service-abstract + accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + keywordlist: + keyword: + - service-keyword + optionalconstraints: + maxheight: 4000 + maxwidth: 4000 + title: service-title + filename: /var/www/config/capabilities_wms_130.xml +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-capabilities-generator-865bt77thd + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml new file mode 100644 index 0000000..c1ed8a3 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/configmap-featureinfo-generator.yaml @@ -0,0 +1,52 @@ +apiVersion: v1 +data: + input.json: |- + { + "Projection": "EPSG:28992", + "AutomaticCasing": false, + "Version": 2, + "Layers": [ + { + "Name": "layer-name", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + }, + { + "Name": "group-child", + "GroupName": "group", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-featureinfo-generator-668mmh48cc + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml new file mode 100644 index 0000000..8409927 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/configmap-init-scripts.yaml @@ -0,0 +1,189 @@ +--- +apiVersion: v1 +data: + gpkg_download.sh: |- + #!/usr/bin/env bash + + set -euo pipefail + + function download_gpkg() { + local gpkg=$1 + local file=$2 + local url=$3 + + if [ -f "$file" ] && [ ! -f "$file".st ]; then + echo msg=\"File already downloaded\" file=\""$file"\" + else + echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" + + # use curl to check if resource exists + # axel blocks on non-existing resources + curl -IfsS "$url" > /dev/null + + echo start "$gpkg" + ret=0 + # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. + axel -n 1 -T 120 -o "$file" "$url" \ + | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ + | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? + + if [ $ret -ne 0 ] + then + echo -e '\n' + # Download failed ($? != 0). + if [ $ret -eq 1 ] + then + # Axel was not able to resume ($? == 1). Remove file and state file. + if [ -f "$file" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file"\" + rm "$file" + fi + if [ -f "$file.st" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file".st\" + rm "$file".st + fi + else + # Download failed with other error ($? > 1). Remove file if state file does not exist. + if [ ! -f "$file.st" ]; then + echo msg=\"Download failed without state file, removing file\" file=\""$file"\" + rm "$file" + fi + fi + + # Retry the download + echo msg=\"Retry file\" file=\""$file"\" + download_gpkg $gpkg $file $url + fi + fi + } + + function download() { + if [ -z "$BLOBS_ENDPOINT" ]; + then + echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; + exit 1; + fi + + local gpkg=$1 + local file=/srv/data/gpkg/$2 + local url=${BLOBS_ENDPOINT}/${gpkg} + + download_gpkg $gpkg $file $url + + # Check Content-length + download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') + file_size=$(wc -c "$file" | awk '{print $1}') + if [ "$download_size" != "$file_size" ] + then + echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + rm_file_and_exit + else + echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + chown 999:999 "$file" + fi + + # Check ogrinfo + echo "Check gpkg with ogrinfo" + if ! ogrinfo -so "$file" + then + echo "ERROR: ogrinfo check on $file failed" + rm_file_and_exit + fi + + # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) + echo "Check if md5 hash value exists in blob storage" + rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" + + # If file contains valid hash, then check it, else skip + hash=$(awk '{ print $1 }' "${file}.md5sum-remote") + if [[ $hash =~ ^[a-f0-9]{32}$ ]] + then + echo "Valid hash value found" + echo "Compare MD5 hash of remote and downloaded gpkg" + if ! (echo "$hash $file" | md5sum --check); then + rm_file_and_exit + fi + else + echo "No hash found for $file in blob storage, skipping checksum." + fi + + echo "done" + } + + function download_all() { + echo msg=\"Starting GeoPackage downloader\" + + local start_time=$(date '+%s') + + # create target location if not exists + mkdir -p /srv/data/gpkg + chown 999:999 /srv/data/gpkg + + download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; + + echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) + } + + function rm_file_and_exit() { + echo "Removing $file, to ensure a fresh new download is started when script is executed again" + rm -rf "$file" + + if [ -f "$file.st" ]; then + rm "$file".st + fi + + echo "Exiting..." + exit 1 + } + + download_all | awk -W interactive ' + BEGIN { + state="idle"; + } + + { + if ($0 != "") { + if ($1 == "start") { + gpkg=$2; + state="downloading"; + } else if ($1 == "done") { + state="idle"; + } else if (state == "downloading") { + if ($1 == "progress") { + # reduce output to prevent loki from choking on large log volume + if (last_percentage != $2) { + if ($3 == "") { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; + } else { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; + } + } + last_percentage=$2; + } else { + print "msg=\"" $0 "\" gpkg=" gpkg; + } + } else { + print $0; + } + } + } + ' +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-init-scripts-f8k8ffgmgh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml new file mode 100644 index 0000000..e3f270a --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/configmap-legend-generator.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +data: + default_mapserver.conf: ... + input: |- + "layer-name" "layer-style-name" + "group" "group" + "group-child" "group-child" +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-legend-generator-82hh8mg962 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml new file mode 100644 index 0000000..8723813 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/configmap-mapfile-generator.yaml @@ -0,0 +1,118 @@ +apiVersion: v1 +data: + input.json: |- + { + "service_title": "service-title", + "service_abstract": "service-abstract", + "service_keywords": "service-keyword", + "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-25000 250000 280000 860000", + "maxSize": "4000", + "service_namespace_prefix": "dataset", + "service_namespace_uri": "http://dataset.geonovum.nl", + "service_onlineresource": "http://localhost", + "service_path": "/datasetOwner/dataset/wms/v1_0", + "service_metadata_id": "metameta-meta-meta-meta-metametameta", + "dataset_owner": "authority-name", + "authority_url": "http://authority-url", + "automatic_casing": false, + "data_epsg": "EPSG:28992", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "templates": "/srv/data/config/templates", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "symbols": [], + "group_layers": [ + { + "name": "group", + "title": "group", + "abstract": "group", + "style_name": "group", + "style_title": "group" + } + ], + "layers": [ + { + "name": "layer-name", + "gpkg_path": "/srv/data/gpkg/file.gpkg", + "tablename": "layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "layer-column" + } + ], + "title": "layer-title", + "abstract": "layer-abstract", + "keywords": "layer-keyword", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "layer_extent": "-25000 250000 280000 860000", + "styles": [ + { + "title": "layer-style-title", + "path": "/styling/layer-style.style" + } + ] + }, + { + "name": "group-child", + "group_name": "group", + "gpkg_path": "/srv/data/gpkg/file.gpkg", + "tablename": "layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "layer-column" + } + ], + "title": "group-child", + "abstract": "group-child", + "keywords": "", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "src-md-id", + "layer_extent": "-25000 250000 280000 860000", + "styles": [ + { + "title": "group-child", + "path": "/styling/layer-style.style" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapfile-generator-2t677hd4f7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..c314789 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/configmap-mapserver.yaml @@ -0,0 +1,146 @@ +--- +apiVersion: v1 +data: + default_mapserver.conf: |- + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/config/mapfile/service.map" + END + END + include.conf: >- + server.modules += ( "mod_status" ) + + + $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { + status.status-url = "/server-status" + } + + + url.rewrite-once = ( + "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", + "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" + ) + + + magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) + + + setenv.add-environment += ( + "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, + ) + ogc.lua: >- + if lighty.r.req_attr["request.method"] == "GET" then + + -- obtain service type from environment + serviceType = os.getenv('SERVICE_TYPE'):lower() + + path = lighty.r.req_attr["uri.path"] + query = lighty.r.req_attr["uri.query"] + + -- handle legend requests + if serviceType == "wms" then + _, _, file = path:find(".*/legend/(.*)") + if file then + if file:find(".*%.png") then + local legendPath = "/var/www/legend/" .. file + local stat = lighty.stat(legendPath) + if (not stat or not stat.is_file) then + -- don't serve non existing legend file + return 404 + end + lighty.content = { { filename = legendPath } } + lighty.header['Content-Type'] = "image/png" + return 200 + end + + return 404 + end + end + + params = {} + if query then + for k, v in query:gmatch("([^?&=]+)=([^&]+)") do + k = k:lower() + + params[k] = v + end + end + + -- assign service and version default values + version = params['version'] + service = params['service'] + + if not service then + service = serviceType + else + service = service:lower() + end + + if (service == 'wms' and (not version or version ~= '1.1.1')) then + version = '1.3.0' + end + + if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then + version = '2.0.0' + end + + -- serve static content + request = params['request'] + if request then + request = request:lower() + + staticStatus = 200 + staticContentType = 'text/xml; charset=UTF-8' + if request == 'getcapabilities' then + if (service == 'wms' and version == '1.3.0') then + staticFile = '/var/www/config/capabilities_wms_130.xml' + elseif (service == 'wfs' and version == '2.0.0') then + staticFile = '/var/www/config/capabilities_wfs_200.xml' + end + elseif service == 'wfs' and request == 'getfeature' then + startindex = params['startindex'] + if startindex and tonumber(startindex) > 50000 then + staticFile = '/srv/mapserver/config/scraping-error.xml' + staticStatus = 400 + end + end + + if staticFile then + lighty.content = { { filename = staticFile } } + lighty.header['Content-Type'] = staticContentType + return staticStatus + end + end + end + scraping-error.xml: >- + + + + + + It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. + + + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapserver-df94mb2d76 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml new file mode 100644 index 0000000..f316d4a --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/configmap-ogc-webservice-proxy.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +data: + service-config.yaml: |- + grouplayers: + group-layer-name: + - gpkg-layer-name + - postgis-layer-name + top-layer-name: + - gpkg-layer-name + - postgis-layer-name + - tif-layer-name + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wms + service-version: v1_0 + name: minimal-wms-ogc-webservice-proxy-22tb5878f7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/minimal/expected/deployment.yaml b/internal/controller/test_data/wms/minimal/expected/deployment.yaml new file mode 100644 index 0000000..37bfbd2 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/deployment.yaml @@ -0,0 +1,364 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: "9117" + prometheus.io/scrape: "true" + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + spec: + restartPolicy: Always + dnsPolicy: ClusterFirst + containers: + - env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + - name: SERVICE_TYPE + value: WMS + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - sleep + - "15" + livenessProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabil\ + ities' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: text/xml'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + name: mapserver + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png' + 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: image/png'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + resources: + limits: + ephemeral-storage: 200M + memory: 800M + requests: + cpu: "0.1" + startupProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name,group,group-child&STYLES=&FORMAT=image/png' + 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: image/png'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - args: + - --scrape_uri=http://localhost/server-status?auto + image: test.test/image:test7 + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: "0.02" + - name: ogc-webservice-proxy + image: test.test/image:test6 + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + limits: + memory: 200M + requests: + cpu: "0.05" + command: + - /ogc-webservice-proxy + - -h=http://127.0.0.1/ + - -t=wms + - -s=/input/service-config.yaml + - -v + - -d=15 + ports: + - containerPort: 9111 + volumeMounts: + - name: ogc-webservice-proxy-config + mountPath: /input + readOnly: true + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + bash /srv/scripts/gpkg_download.sh; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + requests: + cpu: '0.15' + limits: + cpu: '0.2' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - mountPath: /srv/scripts + name: init-scripts + readOnly: true + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:test4 + imagePullPolicy: IfNotPresent + name: capabilities-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --not-include + - wms + - /input/input.json + - /srv/data/config/mapfile + command: + - generate-mapfile + image: test.test/image:test2 + imagePullPolicy: IfNotPresent + name: mapfile-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: mapfile-generator-config + readOnly: true + - mountPath: /styling + name: styling-files + readOnly: true + - args: + - --input-path + - /input/input.json + - --dest-folder + - /srv/data/config/templates + - --file-name + - feature-info + command: + - featureinfo-generator + image: test.test/image:test5 + imagePullPolicy: IfNotPresent + name: featureinfo-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: featureinfo-generator-config + readOnly: true + - command: + - bash + - -c + - | + set -eu; + exit_code=0; + cat /input/input | xargs -n 2 echo | while read layer style; do + echo Generating legend for layer: $layer, style: $style; + mkdir -p /var/www/legend/$layer; + mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; + magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); + if [[ $magic_bytes != 'PNG' ]]; then + echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; + exit_code=1; + fi; + done; + exit $exit_code; + env: + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + name: legend-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/default_mapserver.conf + name: mapserver + subPath: default_mapserver.conf + - mountPath: /input + name: legend-generator-config + readOnly: true + terminationGracePeriodSeconds: 60 + volumes: + - emptyDir: {} + name: base + - emptyDir: {} + name: data + - configMap: + name: minimal-wms-mapserver-df94mb2d76 + defaultMode: 420 + name: mapserver + - configMap: + name: minimal-wms-ogc-webservice-proxy-22tb5878f7 + defaultMode: 420 + name: ogc-webservice-proxy-config + - configMap: + defaultMode: 511 + name: minimal-wms-init-scripts-f8k8ffgmgh + name: init-scripts + - configMap: + name: minimal-wms-capabilities-generator-865bt77thd + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: minimal-wms-mapfile-generator-2t677hd4f7 + defaultMode: 420 + name: mapfile-generator-config + - name: styling-files + projected: + sources: + - configMap: + name: styling + - configMap: + name: minimal-wms-featureinfo-generator-668mmh48cc + defaultMode: 420 + name: featureinfo-generator-config + - configMap: + name: minimal-wms-legend-generator-82hh8mg962 + defaultMode: 420 + name: legend-generator-config diff --git a/internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..fbe9c3e --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 600 + type: Percent + value: 10 + - periodSeconds: 600 + type: Pods + value: 1 + selectPolicy: Max + stabilizationWindowSeconds: 3600 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 0 + maxReplicas: 30 + metrics: + - resource: + name: cpu + target: + averageUtilization: 90 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: minimal-wms-mapserver diff --git a/internal/controller/test_data/wms/minimal/expected/ingressroute.yaml b/internal/controller/test_data/wms/minimal/expected/ingressroute.yaml new file mode 100644 index 0000000..b5d8919 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/ingressroute.yaml @@ -0,0 +1,43 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true + annotations: + "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" + uptime.pdok.nl/id: 6b32f83fa679db692793ba30367d286b3de46f8a + uptime.pdok.nl/name: MINIMAL WMS + uptime.pdok.nl/tags: public-stats,wms + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png +spec: + routes: + - kind: Rule + match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) + middlewares: + - name: minimal-wms-mapserver-headers + services: + - kind: Service + name: minimal-wms-mapserver + port: 80 + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) + middlewares: + - name: minimal-wms-mapserver-headers + services: + - kind: Service + name: minimal-wms-mapserver + port: 9111 diff --git a/internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml b/internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml new file mode 100644 index 0000000..f6642f1 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/middleware-headers.yaml @@ -0,0 +1,26 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..8a5fe6d --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/poddisruptionbudget.yaml @@ -0,0 +1,29 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/minimal/expected/service.yaml b/internal/controller/test_data/wms/minimal/expected/service.yaml new file mode 100644 index 0000000..33b6fc8 --- /dev/null +++ b/internal/controller/test_data/wms/minimal/expected/service.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: minimal-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: minimal + uid: "" + blockOwnerDeletion: true + controller: true +spec: + type: ClusterIP + internalTrafficPolicy: Cluster + sessionAffinity: None + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: ogc-webservice-proxy + port: 9111 + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/minimal/input/ownerinfo.yaml b/internal/controller/test_data/wms/minimal/input/ownerinfo.yaml new file mode 100644 index 0000000..8025b4a --- /dev/null +++ b/internal/controller/test_data/wms/minimal/input/ownerinfo.yaml @@ -0,0 +1,36 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wms: + contactInformation: + contactPersonPrimary: + contactPerson: KlantContactCenter PDOK + contactOrganization: PDOK + contactPosition: pointOfContact + contactAddress: + addressType: + address: + city: Apeldoorn + stateOrProvince: + postCode: + country: Netherlands + contactVoiceTelephone: + contactFacsimileTelephone: + contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/minimal/input/wms.yaml b/internal/controller/test_data/wms/minimal/input/wms.yaml new file mode 100644 index 0000000..a70075d --- /dev/null +++ b/internal/controller/test_data/wms/minimal/input/wms.yaml @@ -0,0 +1,131 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wms + service-version: v1_0 + name: minimal + namespace: default +spec: + options: {} + podSpecPatch: + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + resources: + limits: + ephemeral-storage: 100m + service: + prefix: dataset + abstract: service-abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + keywords: + - service-keyword + layer: + abstract: service-abstract + keywords: + - service-keyword + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + layers: + - abstract: layer-abstract + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: layer-name + styles: + - name: layer-style-name + title: layer-style-title + visualization: layer-style.style + title: layer-title + visible: true + - abstract: group + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + layers: + - abstract: group-child + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: group-child + styles: + - name: group-child + title: group-child + visualization: layer-style.style + - name: style + title: style + visualization: style.style + title: group-child + visible: true + keywords: + - layer-keyword + name: group + title: group + visible: true + styles: + - name: style + title: style + title: service-title + visible: true + ownerInfoRef: owner + stylingAssets: + configMapRefs: + - keys: + - layer-style.style + - style.style + name: styling + title: service-title + url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..662b03d --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,150 @@ +--- +apiVersion: v1 +data: + input.yaml: >- + global: + additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd + http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd + namespace: http://dataset.geonovum.nl + onlineresourceurl: http://localhost + path: /datasetOwner/dataset + prefix: dataset + version: v1_0 + services: + wms130: + definition: + capability: + wmscapabilities: + layer: + - abstract: service-abstract + keywordlist: + keyword: + - service-keyword + layer: + - abstract: layer-abstract + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - layer-keyword + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: layer-name + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: layer-style-name + title: layer-style-title + title: layer-title + - abstract: group + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + layer: + - abstract: group-child + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group-child + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group-child + title: group-child + title: group-child + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group + title: group + title: group + queryable: 1 + title: service-title + service: + abstract: service-abstract + accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + keywordlist: + keyword: + - service-keyword + optionalconstraints: + maxheight: 4000 + maxwidth: 4000 + title: service-title + filename: /var/www/config/capabilities_wms_130.xml +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-capabilities-generator-865bt77thd + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml new file mode 100644 index 0000000..28a5456 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/configmap-featureinfo-generator.yaml @@ -0,0 +1,52 @@ +apiVersion: v1 +data: + input.json: |- + { + "Projection": "EPSG:28992", + "AutomaticCasing": false, + "Version": 2, + "Layers": [ + { + "Name": "layer-name", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + }, + { + "Name": "group-child", + "GroupName": "group", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-featureinfo-generator-668mmh48cc + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml new file mode 100644 index 0000000..42c8d9d --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/configmap-legend-generator.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +data: + default_mapserver.conf: ... + input: |- + "layer-name" "layer-style-name" + "group" "group" + "group-child" "group-child" +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-legend-generator-82hh8mg962 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml new file mode 100644 index 0000000..5b199d8 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/configmap-mapfile-generator.yaml @@ -0,0 +1,118 @@ +apiVersion: v1 +data: + input.json: |- + { + "service_title": "service-title", + "service_abstract": "service-abstract", + "service_keywords": "service-keyword", + "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-25000 250000 280000 860000", + "maxSize": "4000", + "service_namespace_prefix": "dataset", + "service_namespace_uri": "http://dataset.geonovum.nl", + "service_onlineresource": "http://localhost", + "service_path": "/datasetOwner/dataset/wms/v1_0", + "service_metadata_id": "metameta-meta-meta-meta-metametameta", + "dataset_owner": "authority-name", + "authority_url": "http://authority-url", + "automatic_casing": false, + "data_epsg": "EPSG:28992", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "templates": "/srv/data/config/templates", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "symbols": [], + "group_layers": [ + { + "name": "group", + "title": "group", + "abstract": "group", + "style_name": "group", + "style_title": "group" + } + ], + "layers": [ + { + "name": "layer-name", + "gpkg_path": "/srv/data/gpkg/file.gpkg", + "tablename": "layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "layer-column" + } + ], + "title": "layer-title", + "abstract": "layer-abstract", + "keywords": "layer-keyword", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "layer_extent": "-25000 250000 280000 860000", + "styles": [ + { + "title": "layer-style-title", + "path": "/styling/layer-style.style" + } + ] + }, + { + "name": "group-child", + "group_name": "group", + "gpkg_path": "/srv/data/gpkg/file.gpkg", + "tablename": "layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "layer-column" + } + ], + "title": "group-child", + "abstract": "group-child", + "keywords": "", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "src-md-id", + "layer_extent": "-25000 250000 280000 860000", + "styles": [ + { + "title": "group-child", + "path": "/styling/layer-style.style" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapfile-generator-bcd2255b7k + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..5c69f72 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/configmap-mapserver.yaml @@ -0,0 +1,146 @@ +--- +apiVersion: v1 +data: + default_mapserver.conf: |- + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/config/mapfile/service.map" + END + END + include.conf: >- + server.modules += ( "mod_status" ) + + + $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { + status.status-url = "/server-status" + } + + + url.rewrite-once = ( + "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", + "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" + ) + + + magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) + + + setenv.add-environment += ( + "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, + ) + ogc.lua: >- + if lighty.r.req_attr["request.method"] == "GET" then + + -- obtain service type from environment + serviceType = os.getenv('SERVICE_TYPE'):lower() + + path = lighty.r.req_attr["uri.path"] + query = lighty.r.req_attr["uri.query"] + + -- handle legend requests + if serviceType == "wms" then + _, _, file = path:find(".*/legend/(.*)") + if file then + if file:find(".*%.png") then + local legendPath = "/var/www/legend/" .. file + local stat = lighty.stat(legendPath) + if (not stat or not stat.is_file) then + -- don't serve non existing legend file + return 404 + end + lighty.content = { { filename = legendPath } } + lighty.header['Content-Type'] = "image/png" + return 200 + end + + return 404 + end + end + + params = {} + if query then + for k, v in query:gmatch("([^?&=]+)=([^&]+)") do + k = k:lower() + + params[k] = v + end + end + + -- assign service and version default values + version = params['version'] + service = params['service'] + + if not service then + service = serviceType + else + service = service:lower() + end + + if (service == 'wms' and (not version or version ~= '1.1.1')) then + version = '1.3.0' + end + + if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then + version = '2.0.0' + end + + -- serve static content + request = params['request'] + if request then + request = request:lower() + + staticStatus = 200 + staticContentType = 'text/xml; charset=UTF-8' + if request == 'getcapabilities' then + if (service == 'wms' and version == '1.3.0') then + staticFile = '/var/www/config/capabilities_wms_130.xml' + elseif (service == 'wfs' and version == '2.0.0') then + staticFile = '/var/www/config/capabilities_wfs_200.xml' + end + elseif service == 'wfs' and request == 'getfeature' then + startindex = params['startindex'] + if startindex and tonumber(startindex) > 50000 then + staticFile = '/srv/mapserver/config/scraping-error.xml' + staticStatus = 400 + end + end + + if staticFile then + lighty.content = { { filename = staticFile } } + lighty.header['Content-Type'] = staticContentType + return staticStatus + end + end + end + scraping-error.xml: >- + + + + + + It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. + + + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapserver-df94mb2d76 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml new file mode 100644 index 0000000..93e5711 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/configmap-ogc-webservice-proxy.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +data: + service-config.yaml: |- + grouplayers: + group-layer-name: + - gpkg-layer-name + - postgis-layer-name + top-layer-name: + - gpkg-layer-name + - postgis-layer-name + - tif-layer-name + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wms + service-version: v1_0 + name: noprefetch-wms-ogc-webservice-proxy-22tb5878f7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/noprefetch/expected/deployment.yaml b/internal/controller/test_data/wms/noprefetch/expected/deployment.yaml new file mode 100644 index 0000000..b3f1718 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/deployment.yaml @@ -0,0 +1,356 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: "9117" + prometheus.io/scrape: "true" + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + spec: + restartPolicy: Always + dnsPolicy: ClusterFirst + containers: + - env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + - name: SERVICE_TYPE + value: WMS + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - sleep + - "15" + livenessProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&request=GetCapabil\ + ities' 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: text/xml'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + name: mapserver + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png' + 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: image/png'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + resources: + limits: + ephemeral-storage: 200M + memory: 800M + requests: + cpu: "0.1" + startupProbe: + exec: + command: + - /bin/sh + - -c + - "wget -SO- -T 10 -t 2 + 'http://127.0.0.1:80/mapserver?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name,group,group-child&STYLES=&FORMAT=image/png' + 2>&1 | egrep -aiA10 'HTTP/1.1 200' | egrep -i + 'Content-Type: image/png'" + successThreshold: 1 + failureThreshold: 3 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 10 + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - args: + - --scrape_uri=http://localhost/server-status?auto + image: test.test/image:test7 + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: "0.02" + - name: ogc-webservice-proxy + image: test.test/image:test6 + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + limits: + memory: 200M + requests: + cpu: "0.05" + command: + - /ogc-webservice-proxy + - -h=http://127.0.0.1/ + - -t=wms + - -s=/input/service-config.yaml + - -v + - -d=15 + ports: + - containerPort: 9111 + volumeMounts: + - name: ogc-webservice-proxy-config + mountPath: /input + readOnly: true + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + requests: + cpu: '0.15' + limits: + cpu: '0.2' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:test4 + imagePullPolicy: IfNotPresent + name: capabilities-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --not-include + - wms + - /input/input.json + - /srv/data/config/mapfile + command: + - generate-mapfile + image: test.test/image:test2 + imagePullPolicy: IfNotPresent + name: mapfile-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: mapfile-generator-config + readOnly: true + - mountPath: /styling + name: styling-files + readOnly: true + - args: + - --input-path + - /input/input.json + - --dest-folder + - /srv/data/config/templates + - --file-name + - feature-info + command: + - featureinfo-generator + image: test.test/image:test5 + imagePullPolicy: IfNotPresent + name: featureinfo-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: featureinfo-generator-config + readOnly: true + - command: + - bash + - -c + - | + set -eu; + exit_code=0; + cat /input/input | xargs -n 2 echo | while read layer style; do + echo Generating legend for layer: $layer, style: $style; + mkdir -p /var/www/legend/$layer; + mapserv -nh 'QUERY_STRING=SERVICE=WMS&language=dut&version=1.3.0&service=WMS&request=GetLegendGraphic&sld_version=1.1.0&layer='$layer'&format=image/png&STYLE='$style'' > /var/www/legend/$layer/${style}.png; + magic_bytes=$(head -c 4 /var/www/legend/$layer/${style}.png | tail -c 3); + if [[ $magic_bytes != 'PNG' ]]; then + echo [4T2O9] file /var/www/legend/$layer/${style}.png appears to not be a png file; + exit_code=1; + fi; + done; + exit $exit_code; + env: + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:test3 + imagePullPolicy: IfNotPresent + name: legend-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/default_mapserver.conf + name: mapserver + subPath: default_mapserver.conf + - mountPath: /input + name: legend-generator-config + readOnly: true + terminationGracePeriodSeconds: 60 + volumes: + - emptyDir: {} + name: base + - emptyDir: {} + name: data + - configMap: + name: noprefetch-wms-mapserver-df94mb2d76 + defaultMode: 420 + name: mapserver + - configMap: + name: noprefetch-wms-ogc-webservice-proxy-22tb5878f7 + defaultMode: 420 + name: ogc-webservice-proxy-config + - configMap: + name: noprefetch-wms-capabilities-generator-865bt77thd + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: noprefetch-wms-mapfile-generator-bcd2255b7k + defaultMode: 420 + name: mapfile-generator-config + - name: styling-files + projected: + sources: + - configMap: + name: styling + - configMap: + name: noprefetch-wms-featureinfo-generator-668mmh48cc + defaultMode: 420 + name: featureinfo-generator-config + - configMap: + name: noprefetch-wms-legend-generator-82hh8mg962 + defaultMode: 420 + name: legend-generator-config diff --git a/internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..d136a5a --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 600 + type: Percent + value: 10 + - periodSeconds: 600 + type: Pods + value: 1 + selectPolicy: Max + stabilizationWindowSeconds: 3600 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 0 + maxReplicas: 30 + metrics: + - resource: + name: cpu + target: + averageUtilization: 90 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: noprefetch-wms-mapserver diff --git a/internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml b/internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml new file mode 100644 index 0000000..feff2ba --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/ingressroute.yaml @@ -0,0 +1,43 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true + annotations: + "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" + uptime.pdok.nl/id: b05a258104b33de6117b1744cd2b8d2231402508 + uptime.pdok.nl/name: NOPREFETCH WMS + uptime.pdok.nl/tags: public-stats,wms + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png +spec: + routes: + - kind: Rule + match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) + middlewares: + - name: noprefetch-wms-mapserver-headers + services: + - kind: Service + name: noprefetch-wms-mapserver + port: 80 + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) + middlewares: + - name: noprefetch-wms-mapserver-headers + services: + - kind: Service + name: noprefetch-wms-mapserver + port: 9111 diff --git a/internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml b/internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml new file mode 100644 index 0000000..9f2ab12 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/middleware-headers.yaml @@ -0,0 +1,26 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..e5bc079 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/poddisruptionbudget.yaml @@ -0,0 +1,29 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/noprefetch/expected/service.yaml b/internal/controller/test_data/wms/noprefetch/expected/service.yaml new file mode 100644 index 0000000..1e4e448 --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/expected/service.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: noprefetch-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: noprefetch + uid: "" + blockOwnerDeletion: true + controller: true +spec: + type: ClusterIP + internalTrafficPolicy: Cluster + sessionAffinity: None + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: ogc-webservice-proxy + port: 9111 + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml b/internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml new file mode 100644 index 0000000..8025b4a --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/input/ownerinfo.yaml @@ -0,0 +1,36 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wms: + contactInformation: + contactPersonPrimary: + contactPerson: KlantContactCenter PDOK + contactOrganization: PDOK + contactPosition: pointOfContact + contactAddress: + addressType: + address: + city: Apeldoorn + stateOrProvince: + postCode: + country: Netherlands + contactVoiceTelephone: + contactFacsimileTelephone: + contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/noprefetch/input/wms.yaml b/internal/controller/test_data/wms/noprefetch/input/wms.yaml new file mode 100644 index 0000000..de21dca --- /dev/null +++ b/internal/controller/test_data/wms/noprefetch/input/wms.yaml @@ -0,0 +1,132 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wms + service-version: v1_0 + name: noprefetch + namespace: default +spec: + options: + prefetchData: false + podSpecPatch: + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + containers: + - name: mapserver + env: + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: blobs-testtest + resources: + limits: + ephemeral-storage: 100m + service: + prefix: dataset + abstract: service-abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + keywords: + - service-keyword + layer: + abstract: service-abstract + keywords: + - service-keyword + boundingBoxes: + - bbox: + maxx: "280000" + maxy: "860000" + minx: "-25000" + miny: "250000" + crs: EPSG:28992 + layers: + - abstract: layer-abstract + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: layer-name + styles: + - name: layer-style-name + title: layer-style-title + visualization: layer-style.style + title: layer-title + visible: true + - abstract: group + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + layers: + - abstract: group-child + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: group-child + styles: + - name: group-child + title: group-child + visualization: layer-style.style + - name: style + title: style + visualization: style.style + title: group-child + visible: true + keywords: + - layer-keyword + name: group + title: group + visible: true + styles: + - name: style + title: style + title: service-title + visible: true + ownerInfoRef: owner + stylingAssets: + configMapRefs: + - keys: + - layer-style.style + - style.style + name: styling + title: service-title + url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml new file mode 100644 index 0000000..8aff4c3 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/configmap-capabilities-generator.yaml @@ -0,0 +1,150 @@ +--- +apiVersion: v1 +data: + input.yaml: >- + global: + additionalschemalocations: http://inspire.ec.europa.eu/schemas/inspire_dls/1.0 http://inspire.ec.europa.eu/schemas/inspire_dls/1.0/inspire_dls.xsd + http://inspire.ec.europa.eu/schemas/common/1.0 http://inspire.ec.europa.eu/schemas/common/1.0/common.xsd + namespace: http://dataset.geonovum.nl + onlineresourceurl: http://localhost + path: /datasetOwner/dataset + prefix: dataset + version: v1_0 + services: + wms130: + definition: + capability: + wmscapabilities: + layer: + - abstract: service-abstract + keywordlist: + keyword: + - service-keyword + layer: + - abstract: layer-abstract + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: bronbron-bron-bron-bron-bronbronbron + keywordlist: + keyword: + - layer-keyword + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: layer-name + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/layer-name/layer-style-name.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: layer-style-name + title: layer-style-title + title: layer-title + - abstract: group + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + layer: + - abstract: group-child + authorityurl: + name: authority-name + onlineresource: + href: http://authority-url + identifier: + authority: authority-name + value: src-md-id + keywordlist: + keyword: [] + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group-child + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group-child/group-child.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group-child + title: group-child + title: group-child + metadataurl: + - format: text/plain + onlineresource: + href: https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id=datadata-data-data-data-datadatadata + type: simple + xlink: http://www.w3.org/1999/xlink + type: TC211 + name: group + queryable: 1 + style: + - legendurl: + format: image/png + height: 20 + onlineresource: + href: http://localhost/datasetOwner/dataset/wms/v1_0/legend/group/group.png + type: simple + xlink: http://www.w3.org/1999/xlink + width: 78 + name: group + title: group + title: group + queryable: 1 + title: service-title + service: + abstract: service-abstract + accessconstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + keywordlist: + keyword: + - service-keyword + optionalconstraints: + maxheight: 4000 + maxwidth: 4000 + title: service-title + filename: /var/www/config/capabilities_wms_130.xml +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-capabilities-generator-f82hgmbht2 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml new file mode 100644 index 0000000..99c6982 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/configmap-featureinfo-generator.yaml @@ -0,0 +1,52 @@ +apiVersion: v1 +data: + input.json: |- + { + "Projection": "EPSG:28992", + "AutomaticCasing": false, + "Version": 2, + "Layers": [ + { + "Name": "layer-name", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + }, + { + "Name": "group-child", + "GroupName": "group", + "Properties": [ + { + "Name": "fuuid" + }, + { + "Name": "layer-column" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-featureinfo-generator-668mmh48cc + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml b/internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml new file mode 100644 index 0000000..f173138 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/configmap-init-scripts.yaml @@ -0,0 +1,189 @@ +--- +apiVersion: v1 +data: + gpkg_download.sh: |- + #!/usr/bin/env bash + + set -euo pipefail + + function download_gpkg() { + local gpkg=$1 + local file=$2 + local url=$3 + + if [ -f "$file" ] && [ ! -f "$file".st ]; then + echo msg=\"File already downloaded\" file=\""$file"\" + else + echo msg=\"Starting download\" gpkg=\""$gpkg"\" file=\""$file"\" url=\""$url"\" + + # use curl to check if resource exists + # axel blocks on non-existing resources + curl -IfsS "$url" > /dev/null + + echo start "$gpkg" + ret=0 + # Connection timeout causes file corruption https://github.com/axel-download-accelerator/axel/issues/303, therefore we set the timeout to 2 minutes. + axel -n 1 -T 120 -o "$file" "$url" \ + | sed -r 's|\[[[:space:]]*(.*)%\].*\[(.*)/s\]|progress \1 \2|p' \ + | sed -r 's|\[[[:space:]]*(.*)%\].*|progress \1|p' || ret=$? + + if [ $ret -ne 0 ] + then + echo -e '\n' + # Download failed ($? != 0). + if [ $ret -eq 1 ] + then + # Axel was not able to resume ($? == 1). Remove file and state file. + if [ -f "$file" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file"\" + rm "$file" + fi + if [ -f "$file.st" ]; then + echo msg=\"Resume failed, removing file\" file=\""$file".st\" + rm "$file".st + fi + else + # Download failed with other error ($? > 1). Remove file if state file does not exist. + if [ ! -f "$file.st" ]; then + echo msg=\"Download failed without state file, removing file\" file=\""$file"\" + rm "$file" + fi + fi + + # Retry the download + echo msg=\"Retry file\" file=\""$file"\" + download_gpkg $gpkg $file $url + fi + fi + } + + function download() { + if [ -z "$BLOBS_ENDPOINT" ]; + then + echo echo "Empty BLOBS_ENDPOINT variable, start script with 'blobs' configmap"; + exit 1; + fi + + local gpkg=$1 + local file=/srv/data/gpkg/$2 + local url=${BLOBS_ENDPOINT}/${gpkg} + + download_gpkg $gpkg $file $url + + # Check Content-length + download_size=$(curl -sI "$url" | grep -i Content-Length | awk '{print $2}' | tr -d '\r') + file_size=$(wc -c "$file" | awk '{print $1}') + if [ "$download_size" != "$file_size" ] + then + echo msg=\"Content-length mismatch\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + rm_file_and_exit + else + echo msg=\"Content-length match\" file=\""$file"\" file_size=\""$file_size"\" download_size=\""$download_size"\" + chown 999:999 "$file" + fi + + # Check ogrinfo + echo "Check gpkg with ogrinfo" + if ! ogrinfo -so "$file" + then + echo "ERROR: ogrinfo check on $file failed" + rm_file_and_exit + fi + + # Only check md5 hash if a valid md5 value is returned (for large blobs Azure doesn't return a md5sum) + echo "Check if md5 hash value exists in blob storage" + rclone md5sum "blobs:${gpkg}" --output-file "${file}.md5sum-remote" + + # If file contains valid hash, then check it, else skip + hash=$(awk '{ print $1 }' "${file}.md5sum-remote") + if [[ $hash =~ ^[a-f0-9]{32}$ ]] + then + echo "Valid hash value found" + echo "Compare MD5 hash of remote and downloaded gpkg" + if ! (echo "$hash $file" | md5sum --check); then + rm_file_and_exit + fi + else + echo "No hash found for $file in blob storage, skipping checksum." + fi + + echo "done" + } + + function download_all() { + echo msg=\"Starting GeoPackage downloader\" + + local start_time=$(date '+%s') + + # create target location if not exists + mkdir -p /srv/data/gpkg + chown 999:999 /srv/data/gpkg + + download ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg file.gpkg; + + echo msg=\"All GeoPackages downloaded\" total_time_seconds=$(expr $(date '+%s') - $start_time) + } + + function rm_file_and_exit() { + echo "Removing $file, to ensure a fresh new download is started when script is executed again" + rm -rf "$file" + + if [ -f "$file.st" ]; then + rm "$file".st + fi + + echo "Exiting..." + exit 1 + } + + download_all | awk -W interactive ' + BEGIN { + state="idle"; + } + + { + if ($0 != "") { + if ($1 == "start") { + gpkg=$2; + state="downloading"; + } else if ($1 == "done") { + state="idle"; + } else if (state == "downloading") { + if ($1 == "progress") { + # reduce output to prevent loki from choking on large log volume + if (last_percentage != $2) { + if ($3 == "") { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2; + } else { + print "msg=\"Downloading\" gpkg=" gpkg " percentage=" $2 " bytes_per_second=" $3; + } + } + last_percentage=$2; + } else { + print "msg=\"" $0 "\" gpkg=" gpkg; + } + } else { + print $0; + } + } + } + ' +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-init-scripts-f8k8ffgmgh + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml new file mode 100644 index 0000000..ae437bc --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/configmap-legend-generator.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +data: + default_mapserver.conf: ... + input: |- + "layer-name" "layer-style-name" + "group" "group" + "group-child" "group-child" +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-legend-generator-6cf9f5k5h5 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml b/internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml new file mode 100644 index 0000000..9f1bb0f --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/configmap-mapfile-generator.yaml @@ -0,0 +1,118 @@ +apiVersion: v1 +data: + input.json: |- + { + "service_title": "service-title", + "service_abstract": "service-abstract", + "service_keywords": "service-keyword", + "service_accessconstraints": "http://creativecommons.org/publicdomain/zero/1.0/deed.nl", + "service_extent": "-25000 250000 280000 860000", + "maxSize": "4000", + "service_namespace_prefix": "dataset", + "service_namespace_uri": "http://dataset.geonovum.nl", + "service_onlineresource": "http://localhost", + "service_path": "/datasetOwner/dataset/wms/v1_0", + "service_metadata_id": "metameta-meta-meta-meta-metametameta", + "dataset_owner": "authority-name", + "authority_url": "http://authority-url", + "automatic_casing": false, + "data_epsg": "EPSG:28992", + "epsg_list": [ + "EPSG:28992", + "EPSG:25831", + "EPSG:25832", + "EPSG:3034", + "EPSG:3035", + "EPSG:3857", + "EPSG:4258", + "EPSG:4326", + "CRS:84" + ], + "templates": "/srv/data/config/templates", + "outputformat_jpg": "jpg", + "outputformat_png8": "png", + "symbols": [], + "group_layers": [ + { + "name": "group", + "title": "group", + "abstract": "group", + "style_name": "group", + "style_title": "group" + } + ], + "layers": [ + { + "name": "layer-name", + "gpkg_path": "/srv/data/gpkg/file.gpkg", + "tablename": "layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "layer-column" + } + ], + "title": "layer-title", + "abstract": "layer-abstract", + "keywords": "layer-keyword", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "bronbron-bron-bron-bron-bronbronbron", + "layer_extent": "-25000 250000 280000 860000", + "styles": [ + { + "title": "layer-style-title", + "path": "/styling/layer-style.style" + } + ] + }, + { + "name": "group-child", + "group_name": "group", + "gpkg_path": "/srv/data/gpkg/file.gpkg", + "tablename": "layer", + "geometry_type": "Point", + "columns": [ + { + "name": "fuuid" + }, + { + "name": "layer-column" + } + ], + "title": "group-child", + "abstract": "group-child", + "keywords": "", + "dataset_metadata_id": "datadata-data-data-data-datadatadata", + "dataset_source_id": "src-md-id", + "layer_extent": "-25000 250000 280000 860000", + "styles": [ + { + "title": "group-child", + "path": "/styling/layer-style.style" + } + ] + } + ] + } +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapfile-generator-mh72kmt774 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml b/internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml new file mode 100644 index 0000000..5bc57d6 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/configmap-mapserver.yaml @@ -0,0 +1,146 @@ +--- +apiVersion: v1 +data: + default_mapserver.conf: |- + CONFIG + ENV + MS_MAP_NO_PATH "true" + END + MAPS + MAP "/srv/data/config/mapfile/service.map" + END + END + include.conf: >- + server.modules += ( "mod_status" ) + + + $HTTP["remoteip"] =~ "^(127\.0\.0\.1|172\.(1[6-9]|2[0-9]|3[01])\.|10\.|192\.168\.)" { + status.status-url = "/server-status" + } + + + url.rewrite-once = ( + "/datasetOwner/dataset/wms/v1_0/legend(.*)" => "/legend$1", + "/datasetOwner/dataset/wms/v1_0(.*)" => "/mapserver$1" + ) + + + magnet.attract-raw-url-to += ( "/srv/mapserver/config/ogc.lua" ) + + + setenv.add-environment += ( + "AZURE_STORAGE_CONNECTION_STRING" => env.AZURE_STORAGE_CONNECTION_STRING, + ) + ogc.lua: >- + if lighty.r.req_attr["request.method"] == "GET" then + + -- obtain service type from environment + serviceType = os.getenv('SERVICE_TYPE'):lower() + + path = lighty.r.req_attr["uri.path"] + query = lighty.r.req_attr["uri.query"] + + -- handle legend requests + if serviceType == "wms" then + _, _, file = path:find(".*/legend/(.*)") + if file then + if file:find(".*%.png") then + local legendPath = "/var/www/legend/" .. file + local stat = lighty.stat(legendPath) + if (not stat or not stat.is_file) then + -- don't serve non existing legend file + return 404 + end + lighty.content = { { filename = legendPath } } + lighty.header['Content-Type'] = "image/png" + return 200 + end + + return 404 + end + end + + params = {} + if query then + for k, v in query:gmatch("([^?&=]+)=([^&]+)") do + k = k:lower() + + params[k] = v + end + end + + -- assign service and version default values + version = params['version'] + service = params['service'] + + if not service then + service = serviceType + else + service = service:lower() + end + + if (service == 'wms' and (not version or version ~= '1.1.1')) then + version = '1.3.0' + end + + if (service == 'wfs' and (not version or (version ~= '1.0.0' and version ~= '1.1.0'))) then + version = '2.0.0' + end + + -- serve static content + request = params['request'] + if request then + request = request:lower() + + staticStatus = 200 + staticContentType = 'text/xml; charset=UTF-8' + if request == 'getcapabilities' then + if (service == 'wms' and version == '1.3.0') then + staticFile = '/var/www/config/capabilities_wms_130.xml' + elseif (service == 'wfs' and version == '2.0.0') then + staticFile = '/var/www/config/capabilities_wfs_200.xml' + end + elseif service == 'wfs' and request == 'getfeature' then + startindex = params['startindex'] + if startindex and tonumber(startindex) > 50000 then + staticFile = '/srv/mapserver/config/scraping-error.xml' + staticStatus = 400 + end + end + + if staticFile then + lighty.content = { { filename = staticFile } } + lighty.header['Content-Type'] = staticContentType + return staticStatus + end + end + end + scraping-error.xml: >- + + + + + + It is not possible to use a 'startindex' higher than 50.000. When you need to scrape the WFS, please refer to the extracts or the ATOM downloads available for this dataset. + + + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapserver-df94mb2d76 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml b/internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml new file mode 100644 index 0000000..e345ad1 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/configmap-ogc-webservice-proxy.yaml @@ -0,0 +1,31 @@ +apiVersion: v1 +data: + service-config.yaml: |- + grouplayers: + group-layer-name: + - gpkg-layer-name + - postgis-layer-name + top-layer-name: + - gpkg-layer-name + - postgis-layer-name + - tif-layer-name + +immutable: true +kind: ConfigMap +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: 'false' + service-type: wms + service-version: v1_0 + name: patches-wms-ogc-webservice-proxy-22tb5878f7 + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true diff --git a/internal/controller/test_data/wms/patches/expected/deployment.yaml b/internal/controller/test_data/wms/patches/expected/deployment.yaml new file mode 100644 index 0000000..2ecdf4e --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/deployment.yaml @@ -0,0 +1,372 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true +spec: + revisionHistoryLimit: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: 'true' + kubectl.kubernetes.io/default-container: mapserver + match-regex.version-checker.io/mapserver: ^\d\.\d\.\d.*$ + prometheus.io/port: "9117" + prometheus.io/scrape: "true" + priority.version-checker.io/mapserver: "4" + priority.version-checker.io/ogc-webservice-proxy: "4" + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + spec: + restartPolicy: Never + dnsPolicy: None + containers: + - env: + - name: SERVICE_TYPE + value: NONE + - name: MAPSERVER_CONFIG_FILE + value: "/srv/patch/config" + - name: MS_MAPFILE + value: /srv/patch/map.map + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: patch-blobs + - name: PATCH + value: PATCH + image: patch.patch/image:patch + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: + - patch + - "15" + livenessProbe: + exec: + command: + - /bin/sh + - -c + - "patch" + successThreshold: 10 + failureThreshold: 30 + initialDelaySeconds: 200 + periodSeconds: 100 + timeoutSeconds: 100 + name: mapserver + terminationMessagePath: /patch/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 8000 + protocol: SCTP + - name: patch + containerPort: 5000 + protocol: UDP + hostIP: patch + hostPort: 5050 + - containerPort: 80 + protocol: TCP + readinessProbe: + exec: + command: + - /bin/sh + - -c + - "patch" + successThreshold: 10 + failureThreshold: 30 + initialDelaySeconds: 200 + periodSeconds: 100 + timeoutSeconds: 100 + terminationGracePeriodSeconds: 1000 + resources: + limits: + ephemeral-storage: 4000M + memory: 8000M + cpu: "5" + requests: + cpu: "5" + ephemeral-storage: 2000M + memory: 2000M + startupProbe: + exec: + command: + - /bin/sh + - -c + - "patch" + successThreshold: 10 + failureThreshold: 30 + initialDelaySeconds: 200 + periodSeconds: 100 + timeoutSeconds: 100 + volumeMounts: + - mountPath: /srv/data/patch + name: base + readOnly: false + - mountPath: /var/www/patch + name: data + readOnly: false + subPath: /patch + - name: patch + mountPath: /patch + - name: base + mountPath: /srv/data + readOnly: false + - name: data + mountPath: /var/www + - mountPath: /srv/mapserver/config/include.conf + name: mapserver + subPath: include.conf + - mountPath: /srv/mapserver/config/ogc.lua + name: mapserver + subPath: ogc.lua + - name: mapserver + mountPath: /srv/mapserver/config/default_mapserver.conf + subPath: default_mapserver.conf + - mountPath: /srv/mapserver/config/scraping-error.xml + name: mapserver + subPath: scraping-error.xml + - args: + - "patch" + image: patch.patch/image:patch + imagePullPolicy: IfNotPresent + name: apache-exporter + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 9117 + protocol: TCP + resources: + limits: + memory: 48M + requests: + cpu: "0.02" + - name: ogc-webservice-proxy + image: patch.patch/image:patch + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + limits: + memory: 200M + requests: + cpu: "0.05" + command: + - /ogc-webservice-proxy + - -h=http://127.0.0.1/ + - -t=wms + - -s=/input/service-config.yaml + - -v + - -d=15 + ports: + - containerPort: 9111 + volumeMounts: + - name: ogc-webservice-proxy-config + mountPath: /input + readOnly: true + initContainers: + - args: + - | + set -e; + mkdir -p /srv/data/config/; + rclone config create --non-interactive --obscure blobs azureblob endpoint $BLOBS_ENDPOINT account $BLOBS_ACCOUNT key $BLOBS_KEY use_emulator true; + bash /srv/scripts/gpkg_download.sh; + command: + - /bin/sh + - -c + env: + - name: GEOPACKAGE_TARGET_PATH + value: /srv/data/gpkg + - name: GEOPACKAGE_DOWNLOAD_LIST + value: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + image: test.test/image:test1 + imagePullPolicy: IfNotPresent + name: blob-download + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + resources: + requests: + cpu: '3' + limits: + cpu: '5' + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - name: data + mountPath: /var/www + readOnly: false + - mountPath: /srv/scripts + name: init-scripts + readOnly: true + - env: + - name: SERVICECONFIG + value: /input/input.yaml + image: test.test/image:patch5 + imagePullPolicy: Always + name: capabilities-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /input + name: capabilities-generator-config + readOnly: true + - args: + - --not-include + - wms + - /input/input.json + - /srv/data/config/mapfile + command: + - generate-mapfile + image: test.test/image:patch3 + imagePullPolicy: IfNotPresent + name: mapfile-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: mapfile-generator-config + readOnly: true + - mountPath: /styling + name: patch + readOnly: true + - args: + - --input-path + - /input/input.json + - --dest-folder + - /srv/data/config/templates + - --file-name + - feature-info + command: + - featureinfo-generator + image: test.test/image:patch5 + imagePullPolicy: Always + name: featureinfo-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /input + name: featureinfo-generator-config + readOnly: true + resources: + requests: + memory: '300M' + limits: + memory: '500M' + - command: + - bash + - -c + - | + patch; + env: + - name: MAPSERVER_CONFIG_FILE + value: "/srv/mapserver/config/default_mapserver.conf" + - name: MS_MAPFILE + value: /srv/data/config/mapfile/service.map + image: test.test/image:patch + imagePullPolicy: IfNotPresent + name: legend-generator + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /srv/data + name: base + readOnly: false + - mountPath: /var/www + name: data + readOnly: false + - mountPath: /srv/mapserver/config/default_mapserver.conf + name: mapserver + subPath: default_mapserver.conf + - mountPath: /input + name: legend-generator-config + readOnly: true + terminationGracePeriodSeconds: 600 + volumes: + - emptyDir: {} + name: base + - emptyDir: {} + name: data + - configMap: + name: patch + defaultMode: 420 + name: mapserver + - name: patch + csi: + driver: patch + - configMap: + name: patches-wms-ogc-webservice-proxy-22tb5878f7 + defaultMode: 420 + name: ogc-webservice-proxy-config + - configMap: + defaultMode: 511 + name: patches-wms-init-scripts-f8k8ffgmgh + name: init-scripts + - configMap: + name: patches-wms-capabilities-generator-f82hgmbht2 + defaultMode: 420 + name: capabilities-generator-config + - configMap: + name: patches-wms-mapfile-generator-mh72kmt774 + defaultMode: 420 + name: mapfile-generator-config + - name: styling-files + projected: + sources: + - configMap: + name: styling + - configMap: + name: patches-wms-featureinfo-generator-668mmh48cc + defaultMode: 420 + name: featureinfo-generator-config + - configMap: + name: patches-wms-legend-generator-6cf9f5k5h5 + defaultMode: 420 + name: legend-generator-config diff --git a/internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml b/internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..7afbfaf --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/horizontalpodautoscaler.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true +spec: + behavior: + scaleDown: + policies: + - periodSeconds: 30 + type: Pods + value: 1 + selectPolicy: Min + stabilizationWindowSeconds: 50 + scaleUp: + policies: + - periodSeconds: 60 + type: Pods + value: 20 + selectPolicy: Max + stabilizationWindowSeconds: 0 + maxReplicas: 10 + metrics: + - resource: + name: memory + target: + averageUtilization: 90 + type: Utilization + type: Resource + minReplicas: 5 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: patches-wms-mapserver diff --git a/internal/controller/test_data/wms/patches/expected/ingressroute.yaml b/internal/controller/test_data/wms/patches/expected/ingressroute.yaml new file mode 100644 index 0000000..fc947f3 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/ingressroute.yaml @@ -0,0 +1,43 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true + annotations: + "pdok.nl/wms-service-metadata-uuid": "metameta-meta-meta-meta-metametameta" + uptime.pdok.nl/id: 2f02f7c27fd537fcc52564dab35d9967ff9a0b9d + uptime.pdok.nl/name: PATCHES WMS + uptime.pdok.nl/tags: public-stats,wms + uptime.pdok.nl/url: http://localhost:32788/datasetOwner/dataset/wms/v1_0?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=190061.4619730016857,462435.5987861062749,202917.7508707302331,473761.6884966178914&CRS=EPSG:28992&WIDTH=100&HEIGHT=100&LAYERS=layer-name&STYLES=&FORMAT=image/png +spec: + routes: + - kind: Rule + match: Host(`localhost`) && PathPrefix(`/datasetOwner/dataset/wms/v1_0/legend`) + middlewares: + - name: patches-wms-mapserver-headers + services: + - kind: Service + name: patches-wms-mapserver + port: 80 + - kind: Rule + match: Host(`localhost`) && Path(`/datasetOwner/dataset/wms/v1_0`) + middlewares: + - name: patches-wms-mapserver-headers + services: + - kind: Service + name: patches-wms-mapserver + port: 9111 diff --git a/internal/controller/test_data/wms/patches/expected/middleware-headers.yaml b/internal/controller/test_data/wms/patches/expected/middleware-headers.yaml new file mode 100644 index 0000000..0ab983f --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/middleware-headers.yaml @@ -0,0 +1,26 @@ +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapserver-headers + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true +spec: + headers: + customResponseHeaders: + Access-Control-Allow-Headers: Content-Type + Access-Control-Allow-Method: GET, POST, OPTIONS + Access-Control-Allow-Origin: '*' + Cache-Control: public, max-age=3600, no-transform diff --git a/internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml b/internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml new file mode 100644 index 0000000..33c3461 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/poddisruptionbudget.yaml @@ -0,0 +1,29 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true +spec: + maxUnavailable: 1 + selector: + matchLabels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/patches/expected/service.yaml b/internal/controller/test_data/wms/patches/expected/service.yaml new file mode 100644 index 0000000..c873515 --- /dev/null +++ b/internal/controller/test_data/wms/patches/expected/service.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: v1 +kind: Service +metadata: + labels: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 + name: patches-wms-mapserver + namespace: default + ownerReferences: + - apiVersion: pdok.nl/v3 + kind: WMS + name: patches + uid: "" + blockOwnerDeletion: true + controller: true +spec: + type: ClusterIP + internalTrafficPolicy: Cluster + sessionAffinity: None + ports: + - name: mapserver + port: 80 + targetPort: 80 + protocol: TCP + - name: ogc-webservice-proxy + port: 9111 + - name: metric + port: 9117 + targetPort: 9117 + protocol: TCP + selector: + pdok.nl/app: mapserver + dataset: dataset + dataset-owner: datasetOwner + pdok.nl/inspire: "false" + service-type: wms + service-version: v1_0 diff --git a/internal/controller/test_data/wms/patches/input/ownerinfo.yaml b/internal/controller/test_data/wms/patches/input/ownerinfo.yaml new file mode 100644 index 0000000..8025b4a --- /dev/null +++ b/internal/controller/test_data/wms/patches/input/ownerinfo.yaml @@ -0,0 +1,36 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wms: + contactInformation: + contactPersonPrimary: + contactPerson: KlantContactCenter PDOK + contactOrganization: PDOK + contactPosition: pointOfContact + contactAddress: + addressType: + address: + city: Apeldoorn + stateOrProvince: + postCode: + country: Netherlands + contactVoiceTelephone: + contactFacsimileTelephone: + contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/controller/test_data/wms/patches/input/wms.yaml b/internal/controller/test_data/wms/patches/input/wms.yaml new file mode 100644 index 0000000..e42d579 --- /dev/null +++ b/internal/controller/test_data/wms/patches/input/wms.yaml @@ -0,0 +1,255 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + annotations: + pdok.nl/wms-service-metadata-uuid: metameta-meta-meta-meta-metametameta + labels: + dataset: dataset + dataset-owner: datasetOwner + service-type: wms + service-version: v1_0 + name: patches + namespace: default +spec: + options: {} + horizontalPodAutoscalerPatch: + behavior: + scaleDown: + policies: + - periodSeconds: 30 + type: Pods + value: 1 + selectPolicy: Min + stabilizationWindowSeconds: 50 + maxReplicas: 10 + metrics: + - resource: + name: memory + target: + averageUtilization: 90 + type: Utilization + type: Resource + minReplicas: 5 + podSpecPatch: + restartPolicy: Never + dnsPolicy: None + containers: + - env: + - name: SERVICE_TYPE + value: NONE + - name: MAPSERVER_CONFIG_FILE + value: "/srv/patch/config" + - name: MS_MAPFILE + value: /srv/patch/map.map + - name: AZURE_STORAGE_CONNECTION_STRING + valueFrom: + secretKeyRef: + key: AZURE_STORAGE_CONNECTION_STRING + name: patch-blobs + - name: PATCH + value: PATCH + image: patch.patch/image:patch + imagePullPolicy: Always + lifecycle: + preStop: + exec: + command: + - patch + - "15" + livenessProbe: + exec: + command: + - /bin/sh + - -c + - "patch" + successThreshold: 10 + failureThreshold: 30 + initialDelaySeconds: 200 + periodSeconds: 100 + timeoutSeconds: 100 + name: mapserver + terminationMessagePath: /patch/termination-log + terminationMessagePolicy: File + ports: + - containerPort: 8000 + protocol: SCTP + - name: patch + containerPort: 5000 + protocol: UDP + hostIP: patch + hostPort: 5050 + readinessProbe: + exec: + command: + - /bin/sh + - -c + - "patch" + successThreshold: 10 + failureThreshold: 30 + initialDelaySeconds: 200 + periodSeconds: 100 + timeoutSeconds: 100 + terminationGracePeriodSeconds: 1000 + resources: + limits: + ephemeral-storage: 4000M + memory: 8000M + cpu: "5" + requests: + cpu: "5" + ephemeral-storage: 2000M + memory: 2000M + startupProbe: + exec: + command: + - /bin/sh + - -c + - "patch" + successThreshold: 10 + failureThreshold: 30 + initialDelaySeconds: 200 + periodSeconds: 100 + timeoutSeconds: 100 + volumeMounts: + - mountPath: /srv/data/patch + name: base + readOnly: false + - mountPath: /var/www/patch + name: data + readOnly: false + subPath: /patch + - name: patch + mountPath: /patch + - args: + - "patch" + image: patch.patch/image:patch + name: apache-exporter + - name: ogc-webservice-proxy + image: patch.patch/image:patch + initContainers: + - name: blob-download + envFrom: + - configMapRef: + name: blobs-testtest + - secretRef: + name: blobs-testtest + resources: + requests: + cpu: '3' + limits: + cpu: '5' + - image: test.test/image:patch5 + imagePullPolicy: Always + name: capabilities-generator + - image: test.test/image:patch3 + name: mapfile-generator + volumeMounts: + - mountPath: /styling + name: patch + readOnly: true + - image: test.test/image:patch5 + imagePullPolicy: Always + name: featureinfo-generator + resources: + requests: + memory: '300M' + limits: + memory: '500M' + - command: + - bash + - -c + - | + patch; + image: test.test/image:patch + name: legend-generator + terminationGracePeriodSeconds: 600 + volumes: + - configMap: + name: patch + name: mapserver + - name: patch + csi: + driver: patch + service: + abstract: service-abstract + accessConstraints: http://creativecommons.org/publicdomain/zero/1.0/deed.nl + dataEPSG: EPSG:28992 + keywords: + - service-keyword + layer: + abstract: service-abstract + keywords: + - service-keyword + layers: + - abstract: layer-abstract + authority: + name: authority-name + spatialDatasetIdentifier: bronbron-bron-bron-bron-bronbronbron + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: layer-name + styles: + - name: layer-style-name + title: layer-style-title + visualization: layer-style.style + title: layer-title + visible: true + - abstract: group + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + layers: + - abstract: group-child + authority: + name: authority-name + spatialDatasetIdentifier: src-md-id + url: http://authority-url + data: + gpkg: + blobKey: ${BLOBS_GEOPACKAGES_BUCKET}/key/file.gpkg + columns: + - name: layer-column + geometryType: Point + tableName: layer + datasetMetadataUrl: + csw: + metadataIdentifier: datadata-data-data-data-datadatadata + keywords: + - layer-keyword + name: group-child + styles: + - name: group-child + title: group-child + visualization: layer-style.style + title: group-child + visible: true + keywords: + - layer-keyword + name: group + title: group + visible: true + title: service-title + visible: true + ownerInfoRef: owner + stylingAssets: + configMapRefs: + - keys: + - layer-style.style + name: styling + title: service-title + url: http://localhost:32788/datasetOwner/dataset/wms/v1_0 \ No newline at end of file diff --git a/internal/controller/types/types.go b/internal/controller/types/types.go new file mode 100644 index 0000000..34e7d03 --- /dev/null +++ b/internal/controller/types/types.go @@ -0,0 +1,21 @@ +package types + +type HashedConfigMapNames struct { + Mapserver string + InitScripts string + MapfileGenerator string + CapabilitiesGenerator string + OgcWebserviceProxy string + LegendGenerator string + FeatureInfoGenerator string +} + +type Images struct { + MapserverImage string + MultitoolImage string + MapfileGeneratorImage string + CapabilitiesGeneratorImage string + FeatureinfoGeneratorImage string + OgcWebserviceProxyImage string + ApacheExporterImage string +} diff --git a/internal/controller/utils/utils.go b/internal/controller/utils/utils.go new file mode 100644 index 0000000..37fee7e --- /dev/null +++ b/internal/controller/utils/utils.go @@ -0,0 +1,66 @@ +package utils + +import ( + //nolint:gosec + "crypto/sha1" + "encoding/hex" + "io" + + "github.com/pdok/mapserver-operator/internal/controller/constants" + + corev1 "k8s.io/api/core/v1" +) + +type EnvFromSourceType string + +const ( + EnvFromSourceTypeConfigMap EnvFromSourceType = "configMap" + EnvFromSourceTypeSecret EnvFromSourceType = "secret" +) + +func NewEnvFromSource(t EnvFromSourceType, name string) corev1.EnvFromSource { + switch t { + case EnvFromSourceTypeConfigMap: + return corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + }, + } + case EnvFromSourceTypeSecret: + return corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: name, + }, + }, + } + default: + return corev1.EnvFromSource{} + } +} + +func GetBaseVolumeMount() corev1.VolumeMount { + return corev1.VolumeMount{Name: constants.BaseVolumeName, MountPath: "/srv/data"} +} + +func GetDataVolumeMount() corev1.VolumeMount { + return corev1.VolumeMount{Name: constants.DataVolumeName, MountPath: "/var/www", ReadOnly: false} +} + +func GetConfigVolumeMount(volumeName string) corev1.VolumeMount { + return corev1.VolumeMount{Name: volumeName, MountPath: "/input", ReadOnly: true} +} + +func GetMapfileVolumeMount() corev1.VolumeMount { + return corev1.VolumeMount{Name: constants.ConfigMapCustomMapfileVolumeName, MountPath: "/srv/data/config/mapfile"} +} + +func Sha1Hash(v string) string { + //nolint:gosec + s := sha1.New() + _, _ = io.WriteString(s, v) + + return hex.EncodeToString(s.Sum(nil)) +} diff --git a/internal/controller/wfs_controller.go b/internal/controller/wfs_controller.go index b2187f9..7b0aeb4 100644 --- a/internal/controller/wfs_controller.go +++ b/internal/controller/wfs_controller.go @@ -1,17 +1,25 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package controller @@ -19,45 +27,111 @@ package controller import ( "context" + "github.com/pdok/mapserver-operator/internal/controller/types" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorstatus "github.com/pdok/smooth-operator/pkg/status" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" - - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) // WFSReconciler reconciles a WFS object type WFSReconciler struct { client.Client Scheme *runtime.Scheme + Images types.Images } // +kubebuilder:rbac:groups=pdok.nl,resources=wfs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=pdok.nl,resources=wfs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=pdok.nl,resources=wfs/finalizers,verbs=update +// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo,verbs=get;list;watch +// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo/status,verbs=get +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=configmaps;services,verbs=watch;create;get;update;list;delete +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=watch;list;get +// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=watch;create;get;update;list;delete +// +kubebuilder:rbac:groups=traefik.io,resources=ingressroutes;middlewares,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=create;update;delete;list;watch +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/status,verbs=get;update +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by +// The Reconcile function compares the state specified by // the WFS object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile -func (r *WFSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile +func (r *WFSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + lgr := log.FromContext(ctx) + lgr.Info("Starting reconcile for WFS resource", "name", req.NamespacedName) + + // Fetch the WFS instance + wfs := &pdoknlv3.WFS{} + if err = r.Client.Get(ctx, req.NamespacedName, wfs); err != nil { + if apierrors.IsNotFound(err) { + lgr.Info("WFS resource not found", "name", req.NamespacedName) + } else { + lgr.Error(err, "unable to fetch WFS resource", "error", err) + } + return result, client.IgnoreNotFound(err) + } + + lgr.Info("Fetching OwnerInfo", "name", req.NamespacedName) + // Fetch the OwnerInfo instance + ownerInfo := &smoothoperatorv1.OwnerInfo{} + objectKey := client.ObjectKey{ + Namespace: wfs.Namespace, + Name: wfs.Spec.Service.OwnerInfoRef, + } + if err := r.Client.Get(ctx, objectKey, ownerInfo); err != nil { + if apierrors.IsNotFound(err) { + lgr.Info("OwnerInfo resource not found", "name", req.NamespacedName) + } else { + lgr.Error(err, "unable to fetch OwnerInfo resource", "error", err) + } + return result, err + } + + // Recover from a panic so we can add the error to the status of the Atom + defer func() { + if rec := recover(); rec != nil { + err = recoveredPanicToError(rec) + smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wfs, err) + } + }() + + // Check TTL, delete if expired + if ttlExpired(wfs) { + err = r.Client.Delete(ctx, wfs) + + return result, err + } + + ensureLabel(wfs, "pdok.nl/service-type", "wfs") - // TODO(user): your logic here + lgr.Info("creating resources for wfs", "wfs", wfs.Name) + operationResults, err := createOrUpdateAllForWMSWFS(ctx, r, wfs, ownerInfo) + if err != nil { + lgr.Info("failed creating resources for wfs", "wfs", wfs.Name) + smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wfs, err) + return result, err + } + lgr.Info("finished creating resources for wfs", "wfs", wfs.Name) + smoothoperatorstatus.LogAndUpdateStatusFinished(ctx, r.Client, wfs, operationResults) - return ctrl.Result{}, nil + return result, err } // SetupWithManager sets up the controller with the Manager. func (r *WFSReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&pdoknlv3.WFS{}). - Named("wfs"). - Complete(r) + return createControllerManager(mgr, &pdoknlv3.WFS{}).Complete(r) } diff --git a/internal/controller/wfs_controller_test.go b/internal/controller/wfs_controller_test.go index a8796bc..f542db5 100644 --- a/internal/controller/wfs_controller_test.go +++ b/internal/controller/wfs_controller_test.go @@ -1,84 +1,322 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package controller import ( "context" + "fmt" + "os" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/pdok/mapserver-operator/internal/controller/types" + "github.com/pdok/smooth-operator/model" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd + . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/yaml" ) -var _ = Describe("WFS Controller", func() { +var _ = Describe("Testing WFS Controller", func() { + + Context("Testing Mutate functions for Minimal WFS", func() { + testMutates(getWFSReconciler, &pdoknlv3.WFS{}, "minimal") + }) + + Context("Testing Mutate functions for Complete WFS", func() { + testMutates(getWFSReconciler, &pdoknlv3.WFS{}, "complete") + }) + + Context("Testing Mutate functions for WFS with prefetchData false", func() { + testMutates(getWFSReconciler, &pdoknlv3.WFS{}, "noprefetch", "configmap-init-scripts.yaml") + }) + Context("When reconciling a resource", func() { - const resourceName = "test-resource" ctx := context.Background() - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed + inputPath := testPath(pdoknlv3.ServiceTypeWFS, "complete") + "input/" + + testWfs := pdoknlv3.WFS{} + clusterWfs := &pdoknlv3.WFS{} + + objectKeyWfs := k8stypes.NamespacedName{} + + testOwner := smoothoperatorv1.OwnerInfo{} + clusterOwner := &smoothoperatorv1.OwnerInfo{} + + objectKeyOwner := k8stypes.NamespacedName{} + + var expectedResources []struct { + obj client.Object + key k8stypes.NamespacedName } - wfs := &pdoknlv3.WFS{} - - BeforeEach(func() { - By("creating the custom resource for the Kind WFS") - err := k8sClient.Get(ctx, typeNamespacedName, wfs) - if err != nil && errors.IsNotFound(err) { - resource := &pdoknlv3.WFS{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, - // TODO(user): Specify other spec details if needed. - } + + It("Should create a WFS and OwnerInfo resource on the cluster", func() { + + By("Creating a new resource for the Kind WFS") + data, err := readTestFile(inputPath + "wfs.yaml") + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, &testWfs) + Expect(err).NotTo(HaveOccurred()) + Expect(testWfs.Name).Should(Equal("complete")) + + objectKeyWfs = k8stypes.NamespacedName{ + Namespace: testWfs.GetNamespace(), + Name: testWfs.GetName(), + } + + err = k8sClient.Get(ctx, objectKeyWfs, clusterWfs) + Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) + if err != nil && apierrors.IsNotFound(err) { + resource := testWfs.DeepCopy() Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) + } + + By("Creating a new resource for the Kind OwnerInfo") + data, err = os.ReadFile(inputPath + "ownerinfo.yaml") + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, &testOwner) + Expect(err).NotTo(HaveOccurred()) + Expect(testOwner.Name).Should(Equal("owner")) + + objectKeyOwner = k8stypes.NamespacedName{ + Namespace: testOwner.GetNamespace(), + Name: testOwner.GetName(), + } + + err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) + Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) + if err != nil && apierrors.IsNotFound(err) { + resource := testOwner.DeepCopy() + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Get(ctx, objectKeyOwner, clusterOwner)).To(Succeed()) } }) - AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &pdoknlv3.WFS{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) + It("Should reconcile successfully", func() { + controllerReconciler := getWFSReconciler() + + By("Reconciling the WFS") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) Expect(err).NotTo(HaveOccurred()) + }) - By("Cleanup the specific resource instance WFS") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + It("Should create all expected resources", func() { + expectedResources, err := getExpectedObjects(ctx, clusterWfs, true, true) + Expect(err).NotTo(HaveOccurred()) + for _, expectedResource := range expectedResources { + Eventually(func() bool { + err := k8sClient.Get(ctx, k8stypes.NamespacedName{Namespace: expectedResource.GetNamespace(), Name: expectedResource.GetName()}, expectedResource) + return Expect(err).NotTo(HaveOccurred()) + }, "10s", "1s").Should(BeTrue()) + } + }) + + It("Should successfully reconcile after a change in an owned resource", func() { + controllerReconciler := getWFSReconciler() + + By("Getting the original Deployment") + deployment := getBareDeployment(clusterWfs) + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) + return Expect(err).NotTo(HaveOccurred()) + }, "10s", "1s").Should(BeTrue()) + originalRevisionHistoryLimit := *deployment.Spec.RevisionHistoryLimit + expectedRevisionHistoryLimit := 99 + Expect(originalRevisionHistoryLimit).Should(Not(Equal(expectedRevisionHistoryLimit))) + + By("Altering the Deployment") + err := k8sClient.Patch(ctx, deployment, client.RawPatch(k8stypes.MergePatchType, []byte( + fmt.Sprintf(`{"spec": {"revisionHistoryLimit": %d}}`, expectedRevisionHistoryLimit)))) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying that the Deployment was altered") + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) + return Expect(err).NotTo(HaveOccurred()) && + Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(expectedRevisionHistoryLimit)) + }, "10s", "1s").Should(BeTrue()) + + By("Reconciling the WFS again") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying that the Deployment was restored") + Eventually(func() bool { + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) + return Expect(err).NotTo(HaveOccurred()) && + Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(originalRevisionHistoryLimit)) + }, "10s", "1s").Should(BeTrue()) }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") - controllerReconciler := &WFSReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + + It("Should delete PodDisruptionBudget if Min and Max replicas == 1 ", func() { + controllerReconciler := getWFSReconciler() + + By("Setting Min and Max replicas to 1") + + Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) + + resource := clusterWfs.DeepCopy() + + resource.Spec.HorizontalPodAutoscalerPatch.MinReplicas = ptr.To(int32(1)) + resource.Spec.HorizontalPodAutoscalerPatch.MaxReplicas = ptr.To(int32(1)) + + Expect(k8sClient.Update(ctx, resource)).To(Succeed()) + + podDisruptionBudget := getBarePodDisruptionBudget(resource) + + By("Reconciling the WFS") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) + Expect(err).NotTo(HaveOccurred()) + + By("Getting the PodDisruptionBudget") + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(podDisruptionBudget), podDisruptionBudget) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) + Expect(clusterWfs.Status.OperationResults[smoothoperatorutils.GetObjectFullName(k8sClient, podDisruptionBudget)]).To(Equal(controllerutil.OperationResult("deleted"))) + }) + + It("Should not Create PodDisruptionBudget if Min and Max replicas == 1 ", func() { + controllerReconciler := getWFSReconciler() + + By("Getting Cluster WFS Min and Max replicas to 1") + Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) + + Expect(clusterWfs.HorizontalPodAutoscalerPatch().MaxReplicas).To(Equal(ptr.To(int32(1)))) + Expect(clusterWfs.HorizontalPodAutoscalerPatch().MinReplicas).To(Equal(ptr.To(int32(1)))) + + podDisruptionBudget := getBarePodDisruptionBudget(clusterWfs) + + By("Reconciling the WFS") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWfs}) + Expect(err).NotTo(HaveOccurred()) + + By("Getting the PodDisruptionBudget") + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(podDisruptionBudget), podDisruptionBudget) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + Expect(k8sClient.Get(ctx, objectKeyWfs, clusterWfs)).To(Succeed()) + _, ok := clusterWfs.Status.OperationResults[smoothoperatorutils.GetObjectFullName(k8sClient, podDisruptionBudget)] + Expect(ok).To(BeFalse()) + }) + + It("Respects the TTL of the WFS", func() { + By("Creating a new resource for the Kind WFS") + + ttlName := testWfs.GetName() + "-ttl" + ttlWfs := testWfs.DeepCopy() + ttlWfs.Name = ttlName + ttlWfs.Spec.Lifecycle = &model.Lifecycle{TTLInDays: smoothoperatorutils.Pointer(int32(0))} + objectKeyTTLWFS := client.ObjectKeyFromObject(ttlWfs) + + err := k8sClient.Get(ctx, objectKeyTTLWFS, ttlWfs) + Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) + if err != nil && apierrors.IsNotFound(err) { + Expect(k8sClient.Create(ctx, ttlWfs)).To(Succeed()) + } + + // Reconcile + _, err = getWFSReconciler().Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyTTLWFS}) + Expect(err).To(Not(HaveOccurred())) + + // Check the WFS cannot be found anymore + Eventually(func() bool { + err = k8sClient.Get(ctx, objectKeyTTLWFS, ttlWfs) + return apierrors.IsNotFound(err) + }, "10s", "1s").Should(BeTrue()) + + // Not checking owned resources because the test env does not do garbage collection + }) + + It("Should cleanup the cluster", func() { + err := k8sClient.Get(ctx, objectKeyWfs, clusterWfs) + Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance WFS") + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, clusterWfs))).To(Succeed()) + + err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance OwnerInfo") + Expect(k8sClient.Delete(ctx, clusterOwner)).To(Succeed()) + + // the testEnv does not do garbage collection (https://book.kubebuilder.io/reference/envtest#testing-considerations) + By("Cleaning Owned Resources") + for _, d := range expectedResources { + err := k8sClient.Get(ctx, d.key, d.obj) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, d.obj)).To(Succeed()) } + }) + }) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, - }) + Context("When manually validating an incoming CRD", func() { + It("Should not error", func() { + err := smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wfs.pdok.nl") Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. + + filepath := "input/wfs.yaml" + testCases := []string{ + testPath(pdoknlv3.ServiceTypeWFS, "minimal") + filepath, + testPath(pdoknlv3.ServiceTypeWFS, "complete") + filepath, + testPath(pdoknlv3.ServiceTypeWFS, "noprefetch") + filepath, + } + + for _, test := range testCases { + yamlInput, err := readTestFile(test) + Expect(err).NotTo(HaveOccurred()) + + err = smoothoperatorvalidation.ValidateSchema(string(yamlInput)) + Expect(err).NotTo(HaveOccurred()) + } }) }) }) + +func getWFSReconciler() *WFSReconciler { + return &WFSReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Images: types.Images{ + MultitoolImage: testImageName1, + MapfileGeneratorImage: testImageName2, + MapserverImage: testImageName3, + CapabilitiesGeneratorImage: testImageName4, + ApacheExporterImage: testImageName5, + }, + } +} diff --git a/internal/controller/wms_controller.go b/internal/controller/wms_controller.go index 0558be9..e486233 100644 --- a/internal/controller/wms_controller.go +++ b/internal/controller/wms_controller.go @@ -1,17 +1,25 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package controller @@ -19,6 +27,16 @@ package controller import ( "context" + "github.com/pdok/mapserver-operator/internal/controller/types" + + "github.com/pdok/mapserver-operator/internal/controller/featureinfogenerator" + "github.com/pdok/mapserver-operator/internal/controller/legendgenerator" + "github.com/pdok/mapserver-operator/internal/controller/ogcwebserviceproxy" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorstatus "github.com/pdok/smooth-operator/pkg/status" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -27,37 +45,174 @@ import ( pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) +const ( + ogcWebserviceProxyInput = "service-config.yaml" + featureinfoGeneratorInput = "input.json" +) + // WMSReconciler reconciles a WMS object type WMSReconciler struct { client.Client Scheme *runtime.Scheme + Images types.Images } // +kubebuilder:rbac:groups=pdok.nl,resources=wms,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=pdok.nl,resources=wms/status,verbs=get;update;patch // +kubebuilder:rbac:groups=pdok.nl,resources=wms/finalizers,verbs=update +// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo,verbs=get;list;watch +// +kubebuilder:rbac:groups=pdok.nl,resources=ownerinfo/status,verbs= +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=configmaps;services,verbs=watch;create;get;update;list;delete +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=watch;list;get +// +kubebuilder:rbac:groups=traefik.io,resources=ingressroutes;middlewares,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=watch;create;get;update;list;delete +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=create;update;delete;list;watch +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/status,verbs=get;update +// +kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by +// The Reconcile function compares the state specified by // the WMS object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile -func (r *WMSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.0/pkg/reconcile +func (r *WMSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + lgr := log.FromContext(ctx) + lgr.Info("Starting reconcile for WMS resource", "name", req.NamespacedName) + + // Fetch the WMS instance + wms := &pdoknlv3.WMS{} + if err = r.Client.Get(ctx, req.NamespacedName, wms); err != nil { + if apierrors.IsNotFound(err) { + lgr.Info("WMS resource not found", "name", req.NamespacedName) + } else { + lgr.Error(err, "unable to fetch WMS resource", "error", err) + } + return result, client.IgnoreNotFound(err) + } + + lgr.Info("Fetching OwnerInfo", "name", req.NamespacedName) + // Fetch the OwnerInfo instance + ownerInfo := &smoothoperatorv1.OwnerInfo{} + objectKey := client.ObjectKey{ + Namespace: wms.Namespace, + Name: wms.Spec.Service.OwnerInfoRef, + } + if err := r.Client.Get(ctx, objectKey, ownerInfo); err != nil { + if apierrors.IsNotFound(err) { + lgr.Info("OwnerInfo resource not found", "name", req.NamespacedName) + } else { + lgr.Error(err, "unable to fetch OwnerInfo resource", "error", err) + } + return result, client.IgnoreNotFound(err) + } + + // Recover from a panic so we can add the error to the status of the Atom + defer func() { + if rec := recover(); rec != nil { + err = recoveredPanicToError(rec) + smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wms, err) + } + }() + + // Check TTL, delete if expired + if ttlExpired(wms) { + err = r.Client.Delete(ctx, wms) + + return result, err + } + + ensureLabel(wms, "pdok.nl/service-type", "wms") + + lgr.Info("creating resources for wms", "wms", wms.Name) + operationResults, err := createOrUpdateAllForWMSWFS(ctx, r, wms, ownerInfo) + if err != nil { + lgr.Info("failed creating resources for wms", "wms", wms.Name) + smoothoperatorstatus.LogAndUpdateStatusError(ctx, r.Client, wms, err) + return result, err + } + lgr.Info("finished creating resources for wms", "wms", wms.Name) + smoothoperatorstatus.LogAndUpdateStatusFinished(ctx, r.Client, wms, operationResults) + + return result, err +} + +func mutateConfigMapLegendGenerator(r *WMSReconciler, wms *pdoknlv3.WMS, configMap *corev1.ConfigMap) error { + labels := addCommonLabels(wms, smoothoperatorutils.CloneOrEmptyMap(wms.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(r.Client, configMap, labels); err != nil { + return err + } + + if len(configMap.Data) == 0 { + configMap.Data = legendgenerator.GetConfigMapData(wms) + } + configMap.Immutable = smoothoperatorutils.Pointer(true) + + if err := smoothoperatorutils.EnsureSetGVK(r.Client, configMap, configMap); err != nil { + return err + } + if err := ctrl.SetControllerReference(wms, configMap, getReconcilerScheme(r)); err != nil { + return err + } + return smoothoperatorutils.AddHashSuffix(configMap) + +} + +func mutateConfigMapFeatureinfoGenerator(r *WMSReconciler, wms *pdoknlv3.WMS, configMap *corev1.ConfigMap) error { + labels := addCommonLabels(wms, smoothoperatorutils.CloneOrEmptyMap(wms.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(r.Client, configMap, labels); err != nil { + return err + } + + if len(configMap.Data) == 0 { + input, err := featureinfogenerator.GetInput(wms) + if err != nil { + return err + } + configMap.Data = map[string]string{featureinfoGeneratorInput: input} + } + configMap.Immutable = smoothoperatorutils.Pointer(true) + + if err := smoothoperatorutils.EnsureSetGVK(r.Client, configMap, configMap); err != nil { + return err + } + if err := ctrl.SetControllerReference(wms, configMap, r.Scheme); err != nil { + return err + } + return smoothoperatorutils.AddHashSuffix(configMap) +} + +func mutateConfigMapOgcWebserviceProxy(r *WMSReconciler, wms *pdoknlv3.WMS, configMap *corev1.ConfigMap) error { + + labels := addCommonLabels(wms, smoothoperatorutils.CloneOrEmptyMap(wms.GetLabels())) + if err := smoothoperatorutils.SetImmutableLabels(r.Client, configMap, labels); err != nil { + return err + } - // TODO(user): your logic here + if len(configMap.Data) == 0 { + input, err := ogcwebserviceproxy.GetConfig(wms) + if err != nil { + return err + } + configMap.Data = map[string]string{ogcWebserviceProxyInput: input} + } + configMap.Immutable = smoothoperatorutils.Pointer(true) - return ctrl.Result{}, nil + if err := smoothoperatorutils.EnsureSetGVK(r.Client, configMap, configMap); err != nil { + return err + } + if err := ctrl.SetControllerReference(wms, configMap, getReconcilerScheme(r)); err != nil { + return err + } + return smoothoperatorutils.AddHashSuffix(configMap) } // SetupWithManager sets up the controller with the Manager. func (r *WMSReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&pdoknlv3.WMS{}). - Named("wms"). - Complete(r) + return createControllerManager(mgr, &pdoknlv3.WMS{}).Complete(r) } diff --git a/internal/controller/wms_controller_test.go b/internal/controller/wms_controller_test.go index 6e25e8b..5289ddd 100644 --- a/internal/controller/wms_controller_test.go +++ b/internal/controller/wms_controller_test.go @@ -1,84 +1,280 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package controller import ( "context" + "fmt" + "os" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/pdok/mapserver-operator/internal/controller/types" + "github.com/pdok/smooth-operator/model" + smoothoperatorutils "github.com/pdok/smooth-operator/pkg/util" + . "github.com/onsi/ginkgo/v2" //nolint:revive // ginkgo bdd + . "github.com/onsi/gomega" //nolint:revive // ginkgo bdd pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatorvalidation "github.com/pdok/smooth-operator/pkg/validation" + apierrors "k8s.io/apimachinery/pkg/api/errors" + k8stypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/yaml" ) -var _ = Describe("WMS Controller", func() { +var _ = Describe("Testing WMS Controller", func() { + + Context("Testing Mutate functions for Minimal WMS", func() { + testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "minimal") + }) + + Context("Testing Mutate functions for Minimal WMS without prefetch", func() { + testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "noprefetch", "configmap-init-scripts.yaml") + }) + + Context("Testing Mutate functions for Minimal WMS with a custom mapfile", func() { + testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "custom-mapfile", "configmap-mapfile-generator.yaml") + }) + + Context("Testing Mutate functions for Complete WMS", func() { + testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "complete") + }) + + Context("Testing Mutate functions for WMS with extreme patches", func() { + testMutates(getWMSReconciler, &pdoknlv3.WMS{}, "patches") + }) + Context("When reconciling a resource", func() { - const resourceName = "test-resource" ctx := context.Background() - typeNamespacedName := types.NamespacedName{ - Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed + inputPath := testPath(pdoknlv3.ServiceTypeWMS, "complete") + "input/" + + testWMS := pdoknlv3.WMS{} + clusterWMS := &pdoknlv3.WMS{} + + objectKeyWMS := k8stypes.NamespacedName{} + + testOwner := smoothoperatorv1.OwnerInfo{} + clusterOwner := &smoothoperatorv1.OwnerInfo{} + + objectKeyOwner := k8stypes.NamespacedName{} + + var expectedResources []struct { + obj client.Object + key k8stypes.NamespacedName } - wms := &pdoknlv3.WMS{} - - BeforeEach(func() { - By("creating the custom resource for the Kind WMS") - err := k8sClient.Get(ctx, typeNamespacedName, wms) - if err != nil && errors.IsNotFound(err) { - resource := &pdoknlv3.WMS{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: "default", - }, - // TODO(user): Specify other spec details if needed. - } + + It("Should create a WMS and OwnerInfo resource on the cluster", func() { + + By("Creating a new resource for the Kind WMS") + data, err := readTestFile(inputPath + "wms.yaml") + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, &testWMS) + Expect(err).NotTo(HaveOccurred()) + Expect(testWMS.Name).Should(Equal("complete")) + + objectKeyWMS = k8stypes.NamespacedName{ + Namespace: testWMS.GetNamespace(), + Name: testWMS.GetName(), + } + + err = k8sClient.Get(ctx, objectKeyWMS, clusterWMS) + Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) + if err != nil && apierrors.IsNotFound(err) { + resource := testWMS.DeepCopy() + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Get(ctx, objectKeyWMS, clusterWMS)).To(Succeed()) + } + + By("Creating a new resource for the Kind OwnerInfo") + data, err = os.ReadFile(inputPath + "ownerinfo.yaml") + Expect(err).NotTo(HaveOccurred()) + err = yaml.UnmarshalStrict(data, &testOwner) + Expect(err).NotTo(HaveOccurred()) + Expect(testOwner.Name).Should(Equal("owner")) + + objectKeyOwner = k8stypes.NamespacedName{ + Namespace: testOwner.GetNamespace(), + Name: testOwner.GetName(), + } + + err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) + Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) + if err != nil && apierrors.IsNotFound(err) { + resource := testOwner.DeepCopy() Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + Expect(k8sClient.Get(ctx, objectKeyOwner, clusterOwner)).To(Succeed()) } }) - AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. - resource := &pdoknlv3.WMS{} - err := k8sClient.Get(ctx, typeNamespacedName, resource) + It("Should reconcile successfully", func() { + controllerReconciler := getWMSReconciler() + + By("Reconciling the WMS") + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWMS}) Expect(err).NotTo(HaveOccurred()) + }) - By("Cleanup the specific resource instance WMS") - Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + It("Should create all expected resources", func() { + expectedResources, err := getExpectedObjects(ctx, clusterWMS, true, true) + Expect(err).NotTo(HaveOccurred()) + + for _, expectedResource := range expectedResources { + Eventually(func() bool { + err := k8sClient.Get(ctx, k8stypes.NamespacedName{Namespace: expectedResource.GetNamespace(), Name: expectedResource.GetName()}, expectedResource) + return Expect(err).NotTo(HaveOccurred()) + }, "10s", "1s").Should(BeTrue()) + } + }) + + It("Should successfully reconcile after a change in an owned resource", func() { + controllerReconciler := getWMSReconciler() + + By("Getting the original Deployment") + deployment := getBareDeployment(clusterWMS) + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) + return Expect(err).NotTo(HaveOccurred()) + }, "10s", "1s").Should(BeTrue()) + originalRevisionHistoryLimit := *deployment.Spec.RevisionHistoryLimit + expectedRevisionHistoryLimit := 99 + Expect(originalRevisionHistoryLimit).Should(Not(Equal(expectedRevisionHistoryLimit))) + + By("Altering the Deployment") + err := k8sClient.Patch(ctx, deployment, client.RawPatch(k8stypes.MergePatchType, []byte( + fmt.Sprintf(`{"spec": {"revisionHistoryLimit": %d}}`, expectedRevisionHistoryLimit)))) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying that the Deployment was altered") + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) + return Expect(err).NotTo(HaveOccurred()) && + Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(expectedRevisionHistoryLimit)) + }, "10s", "1s").Should(BeTrue()) + + By("Reconciling the WMS again") + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyWMS}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying that the Deployment was restored") + Eventually(func() bool { + err = k8sClient.Get(ctx, client.ObjectKeyFromObject(deployment), deployment) + return Expect(err).NotTo(HaveOccurred()) && + Expect(*deployment.Spec.RevisionHistoryLimit).To(BeEquivalentTo(originalRevisionHistoryLimit)) + }, "10s", "1s").Should(BeTrue()) + }) + + It("Respects the TTL of the WMS", func() { + By("Creating a new resource for the Kind WMS") + + ttlName := testWMS.GetName() + "-ttl" + ttlWms := testWMS.DeepCopy() + ttlWms.Name = ttlName + ttlWms.Spec.Lifecycle = &model.Lifecycle{TTLInDays: smoothoperatorutils.Pointer(int32(0))} + objectKeyTTLWMS := client.ObjectKeyFromObject(ttlWms) + + err := k8sClient.Get(ctx, objectKeyTTLWMS, ttlWms) + Expect(client.IgnoreNotFound(err)).To(Not(HaveOccurred())) + if err != nil && apierrors.IsNotFound(err) { + Expect(k8sClient.Create(ctx, ttlWms)).To(Succeed()) + } + + // Reconcile + _, err = getWMSReconciler().Reconcile(ctx, reconcile.Request{NamespacedName: objectKeyTTLWMS}) + Expect(err).To(Not(HaveOccurred())) + + // Check the WMS cannot be found anymore + Eventually(func() bool { + err = k8sClient.Get(ctx, objectKeyTTLWMS, ttlWms) + return apierrors.IsNotFound(err) + }, "10s", "1s").Should(BeTrue()) + + // Not checking owned resources because the test env does not do garbage collection }) - It("should successfully reconcile the resource", func() { - By("Reconciling the created resource") - controllerReconciler := &WMSReconciler{ - Client: k8sClient, - Scheme: k8sClient.Scheme(), + + It("Should cleanup the cluster", func() { + err := k8sClient.Get(ctx, objectKeyWMS, clusterWMS) + Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance WMS") + Expect(client.IgnoreNotFound(k8sClient.Delete(ctx, clusterWMS))).To(Succeed()) + + err = k8sClient.Get(ctx, objectKeyOwner, clusterOwner) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance OwnerInfo") + Expect(k8sClient.Delete(ctx, clusterOwner)).To(Succeed()) + + // the testEnv does not do garbage collection (https://book.kubebuilder.io/reference/envtest#testing-considerations) + By("Cleaning Owned Resources") + for _, d := range expectedResources { + err := k8sClient.Get(ctx, d.key, d.obj) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, d.obj)).To(Succeed()) } + }) + }) - _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ - NamespacedName: typeNamespacedName, - }) + Context("When manually validating an incoming CRD", func() { + It("Should not error", func() { + err := smoothoperatorvalidation.LoadSchemasForCRD(cfg, "default", "wms.pdok.nl") Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. + + filepath := "input/wms.yaml" + testCases := []string{ + testPath(pdoknlv3.ServiceTypeWMS, "minimal") + filepath, + testPath(pdoknlv3.ServiceTypeWMS, "complete") + filepath, + testPath(pdoknlv3.ServiceTypeWMS, "noprefetch") + filepath, + testPath(pdoknlv3.ServiceTypeWMS, "custom-mapfile") + filepath, + } + + for _, test := range testCases { + yamlInput, err := readTestFile(test) + Expect(err).NotTo(HaveOccurred()) + + err = smoothoperatorvalidation.ValidateSchema(string(yamlInput)) + Expect(err).NotTo(HaveOccurred()) + } }) }) }) + +func getWMSReconciler() *WMSReconciler { + return &WMSReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + Images: types.Images{ + MultitoolImage: testImageName1, + MapfileGeneratorImage: testImageName2, + MapserverImage: testImageName3, + CapabilitiesGeneratorImage: testImageName4, + FeatureinfoGeneratorImage: testImageName5, + OgcWebserviceProxyImage: testImageName6, + ApacheExporterImage: testImageName7, + }, + } +} diff --git a/internal/webhook/v3/shared_webhook.go b/internal/webhook/v3/shared_webhook.go new file mode 100644 index 0000000..19db028 --- /dev/null +++ b/internal/webhook/v3/shared_webhook.go @@ -0,0 +1,114 @@ +package v3 + +import ( + "context" + "encoding/json" + "errors" + "os" + + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + k8stypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + "sigs.k8s.io/yaml" +) + +const ( + samplesPath = "test_data/" +) + +func readOwnerInfo(ownerInfo *smoothoperatorv1.OwnerInfo) error { + data, err := os.ReadFile(samplesPath + "ownerinfo.yaml") + if err != nil { + return err + } + err = yaml.UnmarshalStrict(data, ownerInfo) + if err != nil { + return err + } + return err +} + +func createOwnerInfo(ctx context.Context, c client.Client, ownerInfo *smoothoperatorv1.OwnerInfo) error { + clusterOwner := &smoothoperatorv1.OwnerInfo{} + objectKeyOwner := k8stypes.NamespacedName{ + Namespace: ownerInfo.GetNamespace(), + Name: ownerInfo.GetName(), + } + + err := c.Get(ctx, objectKeyOwner, clusterOwner) + if client.IgnoreNotFound(err) != nil { + return err + } + if err != nil && apierrors.IsNotFound(err) { + resource := ownerInfo.DeepCopy() + err = c.Create(ctx, resource) + if err != nil { + return err + } + err = c.Get(ctx, objectKeyOwner, clusterOwner) + if err != nil { + return err + } + } + return nil +} + +func updateOwnerInfo(ctx context.Context, c client.Client, ownerInfo *smoothoperatorv1.OwnerInfo) error { + clusterOwner := &smoothoperatorv1.OwnerInfo{} + objectKeyOwner := k8stypes.NamespacedName{ + Namespace: ownerInfo.GetNamespace(), + Name: ownerInfo.GetName(), + } + + err := c.Get(ctx, objectKeyOwner, clusterOwner) + if err != nil { + return err + } + + ownerInfo.ResourceVersion = clusterOwner.ResourceVersion + + err = c.Update(ctx, ownerInfo) + if err != nil { + return err + } + + return nil +} + +func getSampleFilename[W pdoknlv3.WMSWFS](webservice W) (string, error) { + switch any(webservice).(type) { + case *pdoknlv3.WFS: + if _, ok := any(webservice).(*pdoknlv3.WFS); ok { + return samplesPath + "v3_wfs.yaml", nil + } + case *pdoknlv3.WMS: + if _, ok := any(webservice).(*pdoknlv3.WMS); ok { + return samplesPath + "v3_wms.yaml", nil + } + } + return "", errors.New("unknown webservice type, cannot determine sample filename") +} + +func readSample[W pdoknlv3.WMSWFS](webservice W) error { + sampleFilename, err := getSampleFilename(webservice) + if err != nil { + return err + } + sampleYaml, err := os.ReadFile(sampleFilename) + if err != nil { + return err + } + sampleJSON, err := yaml.YAMLToJSONStrict(sampleYaml) + if err != nil { + return err + } + err = json.Unmarshal(sampleJSON, webservice) + if err != nil { + return err + } + + return nil +} diff --git a/internal/webhook/v3/test_data/ownerinfo.yaml b/internal/webhook/v3/test_data/ownerinfo.yaml new file mode 100644 index 0000000..76d1eba --- /dev/null +++ b/internal/webhook/v3/test_data/ownerinfo.yaml @@ -0,0 +1,39 @@ +apiVersion: pdok.nl/v1 +kind: OwnerInfo +metadata: + name: owner + namespace: default +spec: + metadataUrls: + csw: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/csw?service=CSW&version=2.0.2&request=GetRecordById&outputschema=http://www.isotc211.org/2005/gmd&elementsetname=full&id={{identifier}}" + type: alternate + openSearch: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/opensearch/dut/{{identifier}}/OpenSearchDescription.xml" + type: alternate + html: + hrefTemplate: "https://www.nationaalgeoregister.nl/geonetwork/srv/dut/catalog.search#/metadata/{{identifier}}" + type: alternate + namespaceTemplate: "http://{{prefix}}.geonovum.nl" + providerSite: + type: simple + href: https://pdok.nl + wfs: + serviceProvider: + providerName: PDOK + wms: + contactInformation: + contactPersonPrimary: + contactPerson: KlantContactCenter PDOK + contactOrganization: PDOK + contactPosition: pointOfContact + contactAddress: + addressType: + address: + city: Apeldoorn + stateOrProvince: + postCode: + country: Netherlands + contactVoiceTelephone: + contactFacsimileTelephone: + contactElectronicMailAddress: BeheerPDOK@kadaster.nl \ No newline at end of file diff --git a/internal/webhook/v3/test_data/v3_wfs.yaml b/internal/webhook/v3/test_data/v3_wfs.yaml new file mode 100644 index 0000000..10a0d6c --- /dev/null +++ b/internal/webhook/v3/test_data/v3_wfs.yaml @@ -0,0 +1,74 @@ +apiVersion: pdok.nl/v3 +kind: WFS +metadata: + name: sample + namespace: default + labels: + sample: sample +spec: + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 2G + ingressRouteUrls: + - url: "http://host/path" + - url: "http://old/path" + service: + inspire: + language: dut + serviceMetadataUrl: + csw: + metadataIdentifier: 655549bd-8c05-4c69-950b-ad1e346dcac9 + spatialDatasetIdentifier: 90af202c-de3a-4fbf-901c-82ae703904e3 + title: "title" + abstract: "abstract" + defaultCrs: "EPSG:28992" + keywords: + - "keyword" + ownerInfoRef: "owner" + prefix: "prefix" + url: "http://host/path" + bbox: + defaultCRS: + maxx: "1" + maxy: "2" + minx: "3" + miny: "4" + featureTypes: + - name: "name" + title: "title" + abstract: "abstract" + keywords: + - "word" + datasetMetadataUrl: + csw: + metadataIdentifier: 8ec62a28-695f-4f46-a9d5-0aeb8363a0e3 + bbox: + defaultCRS: + maxx: "1" + maxy: "2" + minx: "3" + miny: "4" + data: + gpkg: + blobKey: "container/prefix/file.gpkg" + columns: + - name: column + geometryType: Point + tableName: table + - name: "second_name" + title: "title" + abstract: "abstract" + keywords: + - "word" + datasetMetadataUrl: + csw: + metadataIdentifier: 8ec62a28-695f-4f46-a9d5-0aeb8363a0e3 + data: + postgis: + tableName: table + geometryType: Point + columns: + - name: column diff --git a/internal/webhook/v3/test_data/v3_wms.yaml b/internal/webhook/v3/test_data/v3_wms.yaml new file mode 100644 index 0000000..6ea323c --- /dev/null +++ b/internal/webhook/v3/test_data/v3_wms.yaml @@ -0,0 +1,75 @@ +apiVersion: pdok.nl/v3 +kind: WMS +metadata: + name: sample + namespace: default + labels: + sample: sample +spec: + podSpecPatch: + containers: + - name: mapserver + resources: + limits: + ephemeral-storage: 1G + ingressRouteUrls: + - url: "https://test.test/path" + service: + prefix: "prefix" + url: "https://test.test/path" + title: "title" + abstract: "abstract" + keywords: + - "keyword" + ownerInfoRef: "owner" + dataEPSG: "EPSG:28992" + stylingAssets: + configMapRefs: + - name: configmap + keys: + - file.style + layer: + title: "title" + abstract: "abstract" + keywords: + - "keyword" + visible: true + layers: + - name: "visible" + visible: true + title: "title" + abstract: "abstract" + keywords: + - keyword + data: + gpkg: + blobKey: "container/path/file.gpkg" + columns: + - name: "column" + geometryType: "Point" + tableName: "table" + styles: + - name: "name" + title: "title" + visualization: "file.style" + - name: "visible Group Layer" + visible: true + title: title + abstract: abstract + keywords: + - keyword + styles: + - name: style + title: style + layers: + - name: "not visible" + visible: false + data: + postgis: + columns: + - name: "column" + geometryType: "Point" + tableName: "table" + styles: + - name: "style" + visualization: "file.style" diff --git a/internal/webhook/v3/webhook_suite_test.go b/internal/webhook/v3/webhook_suite_test.go index 8486337..ea12f53 100644 --- a/internal/webhook/v3/webhook_suite_test.go +++ b/internal/webhook/v3/webhook_suite_test.go @@ -1,35 +1,58 @@ /* -Copyright 2025. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v3 +//nolint:revive // Complains about the dot imports import ( "context" "crypto/tls" + "encoding/json" + "errors" "fmt" "net" "os" + "os/exec" "path/filepath" "testing" "time" + "github.com/pdok/smooth-operator/pkg/validation" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + "golang.org/x/tools/go/packages" + + pdoknlv2beta1 "github.com/pdok/mapserver-operator/api/v2beta1" + "k8s.io/apimachinery/pkg/runtime" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,6 +63,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" + admissionv1 "k8s.io/api/admission/v1" // +kubebuilder:scaffold:imports ) @@ -63,18 +87,39 @@ func TestAPIs(t *testing.T) { var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + //nolint:fatcontext ctx, cancel = context.WithCancel(context.TODO()) + scheme := runtime.NewScheme() var err error - err = pdoknlv3.AddToScheme(scheme.Scheme) + err = pdoknlv2beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = pdoknlv3.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = smoothoperatorv1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") + ownerInfoCRDPath := must(getOwnerInfoCRDPath()) testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + Scheme: scheme, + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wfs.yaml"), + filepath.Join("..", "..", "config", "crd", "bases", "pdok.nl_wms.yaml"), + ownerInfoCRDPath, + }, ErrorIfCRDPathMissing: false, + CRDInstallOptions: envtest.CRDInstallOptions{ + Scheme: scheme, + // MaxTime: time.Minute, + }, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, @@ -91,14 +136,14 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) // start webhook server using Manager. webhookInstallOptions := &testEnv.WebhookInstallOptions mgr, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme.Scheme, + Scheme: scheme, WebhookServer: webhook.NewServer(webhook.Options{ Host: webhookInstallOptions.LocalServingHost, Port: webhookInstallOptions.LocalServingPort, @@ -162,3 +207,44 @@ func getFirstFoundEnvTestBinaryDir() string { } return "" } + +func getOwnerInfoCRDPath() (string, error) { + smoothOperatorModule, err := getModule("github.com/pdok/smooth-operator") + if err != nil { + return "", err + } + if smoothOperatorModule.Dir == "" { + return "", errors.New("cannot find path for smooth-operator module") + } + return filepath.Join(smoothOperatorModule.Dir, "config", "crd", "bases", "pdok.nl_ownerinfo.yaml"), nil +} + +func getModule(name string) (module *packages.Module, err error) { + out, err := exec.Command("go", "list", "-json", "-m", name).Output() + if err != nil { + return + } + module = &packages.Module{} + err = json.Unmarshal(out, module) + return +} + +func must[T any](t T, err error) T { + if err != nil { + panic(err) + } + return t +} + +func getValidationError[O pdoknlv3.WMSWFS](obj O, errorList *field.Error) error { + return apierrors.NewInvalid(obj.GroupKind(), obj.GetName(), field.ErrorList{errorList}) +} + +func getValidationWarnings[O pdoknlv3.WMSWFS](obj O, path field.Path, warning string, warnings []string) admission.Warnings { + validation.AddWarning(&warnings, path, warning, schema.GroupVersionKind{ + Group: obj.GroupKind().Group, + Version: "v3", + Kind: obj.GroupKind().Kind, + }, obj.GetName()) + return warnings +} diff --git a/internal/webhook/v3/wfs_webhook.go b/internal/webhook/v3/wfs_webhook.go index ccbfca4..cae035e 100644 --- a/internal/webhook/v3/wfs_webhook.go +++ b/internal/webhook/v3/wfs_webhook.go @@ -1,25 +1,36 @@ /* -Copyright 2025. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +MIT License + +Copyright (c) 2024 Publieke Dienstverlening op de Kaart + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ +//nolint:dupl package v3 import ( "context" "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -29,19 +40,16 @@ import ( pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) -// nolint:unused // log is for logging in this package. -var wfslog = logf.Log.WithName("wfs-resource") +var wfsLog = logf.Log.WithName("wfs-resource") // SetupWFSWebhookWithManager registers the webhook for WFS in the manager. func SetupWFSWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr).For(&pdoknlv3.WFS{}). - WithValidator(&WFSCustomValidator{}). + WithValidator(&WFSCustomValidator{mgr.GetClient()}). Complete() } -// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! - // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. // NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. // Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. @@ -53,44 +61,44 @@ func SetupWFSWebhookWithManager(mgr ctrl.Manager) error { // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as this struct is used only for temporary operations and does not need to be deeply copied. type WFSCustomValidator struct { - // TODO(user): Add more fields as needed for validation + Client client.Client } var _ webhook.CustomValidator = &WFSCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { wfs, ok := obj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object but got %T", obj) } - wfslog.Info("Validation for WFS upon creation", "name", wfs.GetName()) - - // TODO(user): fill in your validation logic upon object creation. + wfsLog.Info("Validation for WFS upon creation", "name", wfs.GetName()) - return nil, nil + return wfs.ValidateCreate(v.Client) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { wfs, ok := newObj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object for the newObj but got %T", newObj) } - wfslog.Info("Validation for WFS upon update", "name", wfs.GetName()) - - // TODO(user): fill in your validation logic upon object update. + wfsOld, ok := oldObj.(*pdoknlv3.WFS) + if !ok { + return nil, fmt.Errorf("expected a WFS object for the oldObj but got %T", newObj) + } + wfsLog.Info("Validation for WFS upon update", "name", wfs.GetName()) - return nil, nil + return wfs.ValidateUpdate(v.Client, wfsOld) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { wfs, ok := obj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object but got %T", obj) } - wfslog.Info("Validation for WFS upon deletion", "name", wfs.GetName()) + wfsLog.Info("Validation for WFS upon deletion", "name", wfs.GetName()) // TODO(user): fill in your validation logic upon object deletion. diff --git a/internal/webhook/v3/wfs_webhook_test.go b/internal/webhook/v3/wfs_webhook_test.go index 93abd49..c406c9b 100644 --- a/internal/webhook/v3/wfs_webhook_test.go +++ b/internal/webhook/v3/wfs_webhook_test.go @@ -1,27 +1,45 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v3 +//nolint:revive // Complains about the dot imports import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - // TODO (user): Add any additional imports if needed + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" + smoothoperatormodel "github.com/pdok/smooth-operator/model" + corev1 "k8s.io/api/core/v1" ) var _ = Describe("WFS Webhook", func() { @@ -29,53 +47,421 @@ var _ = Describe("WFS Webhook", func() { obj *pdoknlv3.WFS oldObj *pdoknlv3.WFS validator WFSCustomValidator + ownerInfo *smoothoperatorv1.OwnerInfo ) BeforeEach(func() { - obj = &pdoknlv3.WFS{} - oldObj = &pdoknlv3.WFS{} - validator = WFSCustomValidator{} + validator = WFSCustomValidator{k8sClient} Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") - Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + + sample := &pdoknlv3.WFS{} + err := readSample(sample) + Expect(err).To(BeNil(), "Reading and parsing the WFS V3 sample failed") + + obj = sample.DeepCopy() + oldObj = sample.DeepCopy() + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + + ownerInfoSample := &smoothoperatorv1.OwnerInfo{} + Expect(readOwnerInfo(ownerInfoSample)).To(Succeed(), "Reading and parsing the Ownerinfo sample failed") + ownerInfo = ownerInfoSample.DeepCopy() + Expect(ownerInfo).NotTo(BeNil()) + Expect(createOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + }) AfterEach(func() { - // TODO (user): Add any teardown logic common to all tests + Expect(k8sClient.Delete(ctx, ownerInfo)).To(Succeed()) }) Context("When creating or updating WFS under Validating Webhook", func() { - // TODO (user): Add logic for validating webhooks - // Example: - // It("Should deny creation if a required field is missing", func() { - // By("simulating an invalid creation scenario") - // obj.SomeRequiredField = "" - // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) - // }) - // - // It("Should admit creation if all required fields are present", func() { - // By("simulating an invalid creation scenario") - // obj.SomeRequiredField = "valid_value" - // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) - // }) - // - // It("Should validate updates correctly", func() { - // By("simulating a valid update scenario") - // oldObj.SomeRequiredField = "updated_value" - // obj.SomeRequiredField = "updated_value" - // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) - // }) - }) + ctx := context.Background() + + It("Creates the WFS from the sample", func() { + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny creation if there are no labels", func() { + obj.Labels = nil + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("metadata").Child("labels"), + "can't be empty", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when URL not in IngressRouteURLs", func() { + url, err := smoothoperatormodel.ParseURL("http://changed/changed") + Expect(err).To(BeNil()) + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} + url, err = smoothoperatormodel.ParseURL("http://sample/sample") + Expect(err).To(BeNil()) + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("must contain baseURL: %s", url), + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Warns if the name contains WFS", func() { + obj.Name += "-wfs" + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("metadata").Child("name"), + "name should not contain wfs", + []string{}, + ))) + }) + + It("Should deny creation if there is no bounding box and the defaultCRS is not EPSG:28992", func() { + obj.Spec.Service.DefaultCrs = "EPSG:1234" + obj.Spec.Service.Bbox = nil + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("bbox").Child("defaultCRS"), + "when service.defaultCRS is not 'EPSG:28992'", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Warns if the mapfile and service/featuretype bbox are both set", func() { + Expect(obj.Spec.Service.FeatureTypes[0].Bbox).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].Bbox.DefaultCRS).NotTo(BeNil()) + Expect(obj.Spec.Service.Bbox).NotTo(BeNil()) + obj.Spec.Service.Mapfile = &pdoknlv3.Mapfile{} + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("featureTypes").Index(0).Child("bbox").Child("defaultCrs"), + "is not used when service.mapfile is configured", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("bbox"), + "is not used when service.mapfile is configured", + []string{}, + )))) + }) + + It("Should deny Create when a otherCrs has the same crs multiple times", func() { + crs := "EPSG:3035" + obj.Spec.Service.OtherCrs = []string{crs, crs} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Duplicate( + field.NewPath("spec").Child("service").Child("otherCrs").Index(1), + crs, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should warn on creation if SpatialID is also used as a featureType datasetMetadataID", func() { + Expect(obj.Inspire()).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) + obj.Spec.Service.Inspire.SpatialDatasetIdentifier = obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW.MetadataIdentifier + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(admission.Warnings{field.Invalid( + field.NewPath("spec").Child("service").Child("inspire").Child("spatialDatasetIdentifier"), + obj.Spec.Service.Inspire.SpatialDatasetIdentifier, + "spatialDatasetIdentifier should not also be used as an datasetMetadataUrl.csw.metadataIdentifier", + ).Error()})) + }) + + It("Should deny creation if serviceMetadataID is also used as a featureType datasetMetadataID", func() { + Expect(obj.Inspire()).NotTo(BeNil()) + Expect(obj.Inspire().ServiceMetadataURL.CSW).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) + obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier = obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW.MetadataIdentifier + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), + obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier, + "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as an datasetMetadataUrl.csw.metadataIdentifier", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny creation if serviceMetadataID is the same as the SpatialID", func() { + Expect(obj.Inspire()).NotTo(BeNil()) + Expect(obj.Inspire().ServiceMetadataURL.CSW).NotTo(BeNil()) + obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier = obj.Spec.Service.Inspire.SpatialDatasetIdentifier + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), + obj.Spec.Service.Inspire.ServiceMetadataURL.CSW.MetadataIdentifier, + "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as the spatialDatasetIdentifier", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny creation if service is Inspire and not all featureTypes have the same datasetMetadataID", func() { + Expect(obj.Inspire()).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW).NotTo(BeNil()) + Expect(len(obj.Spec.Service.FeatureTypes)).To(BeNumerically(">", 1)) + Expect(obj.Spec.Service.FeatureTypes[1].DatasetMetadataURL).NotTo(BeNil()) + Expect(obj.Spec.Service.FeatureTypes[1].DatasetMetadataURL.CSW).NotTo(BeNil()) + obj.Spec.Service.FeatureTypes[0].DatasetMetadataURL.CSW.MetadataIdentifier = "" + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("featureTypes[*]").Child("datasetMetadataUrl").Child("csw").Child("metadataIdentifier"), + obj.DatasetMetadataIDs(), + "when Inspire, all featureTypes need use the same datasetMetadataUrl.csw.metadataIdentifier", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when minReplicas are larger than maxReplicas", func() { + obj.Spec.HorizontalPodAutoscalerPatch = &pdoknlv3.HorizontalPodAutoscalerPatch{ + MinReplicas: ptr.To(int32(10)), + MaxReplicas: ptr.To(int32(5)), + } + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("horizontalPodAutoscaler"), + fmt.Sprintf("minReplicas: %d, maxReplicas: %d", 10, 5), + "maxReplicas cannot be less than minReplicas", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when mapserver container doesn't have ephemeral storage", func() { + obj.Spec.PodSpecPatch = corev1.PodSpec{} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required(field.NewPath("spec"). + Child("podSpecPatch"). + Child("containers"). + Key("mapserver"). + Child("resources"). + Child("limits"). + Child(corev1.ResourceEphemeralStorage.String()), "")))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny creation if multiple featureTypes have the same name", func() { + Expect(len(obj.Spec.Service.FeatureTypes)).To(BeNumerically(">", 1)) + obj.Spec.Service.FeatureTypes[1].Name = obj.Spec.Service.FeatureTypes[0].Name + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Duplicate( + field.NewPath("spec").Child("service").Child("featureTypes").Index(1).Child("name"), + obj.Spec.Service.FeatureTypes[1].Name, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef doesn't exist", func() { + obj.Spec.Service.OwnerInfoRef = "changed" + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.NotFound( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + obj.Spec.Service.OwnerInfoRef, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef misses namespaceTemplate", func() { + ownerInfo.Spec.NamespaceTemplate = nil + + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.namespaceTemplate missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef misses csw metadataTemplate", func() { + obj.Spec.Service.Inspire = &pdoknlv3.WFSInspire{Inspire: pdoknlv3.Inspire{ServiceMetadataURL: pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}}}} + ownerInfo.Spec.MetadataUrls.CSW = nil + + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.metadataUrls.csw missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + + ownerInfo.Spec.MetadataUrls = nil + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err = validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.metadataUrls.csw missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef misses WMS", func() { + ownerInfo.Spec.WFS = nil + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.WFS missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a ingressRouteURL was removed", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).To(BeNil()) + oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ + {URL: obj.URL()}, + {URL: smoothoperatormodel.URL{URL: url}}, + } + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: obj.URL()}} + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("urls cannot be removed, missing: %s", smoothoperatormodel.IngressRouteURL{URL: smoothoperatormodel.URL{URL: url}}), + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should accept update if a url was changed when it's in ingressRouteUrls", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).To(BeNil()) + oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ + {URL: obj.URL()}, + {URL: smoothoperatormodel.URL{URL: url}}, + } + obj.Spec.IngressRouteURLs = oldObj.Spec.IngressRouteURLs + oldObj.Spec.Service.URL = obj.URL() + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a url was changed and ingressRouteUrls = nil", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).To(BeNil()) + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + obj.Spec.IngressRouteURLs = nil + oldObj.Spec.IngressRouteURLs = nil + + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("spec").Child("service").Child("url"), + "is immutable, add the old and new urls to spec.ingressRouteUrls in order to change this field", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update url was changed but not added to ingressRouteURLs", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).ToNot(HaveOccurred()) + oldObj.Spec.IngressRouteURLs = nil + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: oldObj.Spec.Service.URL}} + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("must contain baseURL: %s", obj.URL()), + )))) + Expect(warnings).To(BeEmpty()) + + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} + warnings, err = validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("must contain baseURL: %s", oldObj.URL()), + )))) + Expect(warnings).To(BeEmpty()) + + }) + + It("Should deny update if a label was removed", func() { + oldKey := "" + for label := range obj.Labels { + oldKey = label + delete(obj.Labels, label) + break + } + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("metadata").Child("labels").Child(oldKey), + "labels cannot be removed", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a label changed", func() { + oldKey := "" + oldValue := "" + newValue := "" + for label, val := range obj.Labels { + oldKey = label + oldValue = val + newValue = val + "-newval" + obj.Labels[label] = newValue + break + } + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("metadata").Child("labels").Child(oldKey), + newValue, + "immutable: should be: "+oldValue, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a label was added", func() { + newKey := "new-label" + obj.Labels[newKey] = "test" + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("metadata").Child("labels").Child(newKey), + "new labels cannot be added", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if an inspire block was added", func() { + obj.Spec.Service.Inspire = &pdoknlv3.WFSInspire{} + oldObj.Spec.Service.Inspire = nil + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("spec").Child("service").Child("inspire"), + "cannot change from inspire to not inspire or the other way around", + )))) + Expect(warnings).To(BeEmpty()) + }) - Context("When creating WFS under Conversion Webhook", func() { - // TODO (user): Add logic to convert the object to the desired version and verify the conversion - // Example: - // It("Should convert the object correctly", func() { - // convertedObj := &pdoknlv3.WFS{} - // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) - // Expect(convertedObj).ToNot(BeNil()) - // }) + It("Should deny update if an inspire block was removed", func() { + oldObj.Spec.Service.Inspire = &pdoknlv3.WFSInspire{} + obj.Spec.Service.Inspire = nil + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("spec").Child("service").Child("inspire"), + "cannot change from inspire to not inspire or the other way around", + )))) + Expect(warnings).To(BeEmpty()) + }) }) }) diff --git a/internal/webhook/v3/wms_webhook.go b/internal/webhook/v3/wms_webhook.go index e5a846a..d5befee 100644 --- a/internal/webhook/v3/wms_webhook.go +++ b/internal/webhook/v3/wms_webhook.go @@ -1,36 +1,106 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ +//nolint:dupl package v3 import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) -// nolint:unused // log is for logging in this package. -var wmslog = logf.Log.WithName("wms-resource") +var wmsLog = logf.Log.WithName("wms-resource") // SetupWMSWebhookWithManager registers the webhook for WMS in the manager. func SetupWMSWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr).For(&pdoknlv3.WMS{}). + WithValidator(&WMSCustomValidator{mgr.GetClient()}). Complete() } -// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-pdok-nl-v3-wms,mutating=false,failurePolicy=fail,sideEffects=None,groups=pdok.nl,resources=wms,verbs=create;update,versions=v3,name=vwms-v3.kb.io,admissionReviewVersions=v1 + +// WMSCustomValidator struct is responsible for validating the WMS resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type WMSCustomValidator struct { + Client client.Client +} + +var _ webhook.CustomValidator = &WMSCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type WMS. +func (v *WMSCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + wms, ok := obj.(*pdoknlv3.WMS) + if !ok { + return nil, fmt.Errorf("expected a WMS object but got %T", obj) + } + wmsLog.Info("Validation for WMS upon creation", "name", wms.GetName()) + + return wms.ValidateCreate(v.Client) +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type WMS. +func (v *WMSCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + wms, ok := newObj.(*pdoknlv3.WMS) + if !ok { + return nil, fmt.Errorf("expected a WMS object for the newObj but got %T", newObj) + } + wmsOld, ok := oldObj.(*pdoknlv3.WMS) + if !ok { + return nil, fmt.Errorf("expected a WMS object for the oldObj but got %T", newObj) + } + wmsLog.Info("Validation for WMS upon update", "name", wms.GetName()) + + return wms.ValidateUpdate(v.Client, wmsOld) +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type WMS. +func (v *WMSCustomValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + wms, ok := obj.(*pdoknlv3.WMS) + if !ok { + return nil, fmt.Errorf("expected a WMS object but got %T", obj) + } + wmsLog.Info("Validation for WMS upon deletion", "name", wms.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/internal/webhook/v3/wms_webhook_test.go b/internal/webhook/v3/wms_webhook_test.go index 701c32b..eb54622 100644 --- a/internal/webhook/v3/wms_webhook_test.go +++ b/internal/webhook/v3/wms_webhook_test.go @@ -1,55 +1,677 @@ /* -Copyright 2025. +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) 2024 Publieke Dienstverlening op de Kaart - http://www.apache.org/licenses/LICENSE-2.0 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ package v3 +//nolint:revive // Complains about the dot imports import ( + "context" + "fmt" + + smoothoperatormodel "github.com/pdok/smooth-operator/model" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" - // TODO (user): Add any additional imports if needed + smoothoperatorv1 "github.com/pdok/smooth-operator/api/v1" ) var _ = Describe("WMS Webhook", func() { var ( - obj *pdoknlv3.WMS - oldObj *pdoknlv3.WMS + obj *pdoknlv3.WMS + oldObj *pdoknlv3.WMS + validator WMSCustomValidator + ownerInfo *smoothoperatorv1.OwnerInfo ) BeforeEach(func() { - obj = &pdoknlv3.WMS{} - oldObj = &pdoknlv3.WMS{} - Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + validator = WMSCustomValidator{k8sClient} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + + sample := &pdoknlv3.WMS{} + Expect(readSample(sample)).To(Succeed(), "Reading and parsing the WMS V3 sample failed") + + obj = sample.DeepCopy() + oldObj = sample.DeepCopy() + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") - // TODO (user): Add any setup logic common to all tests + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + + ownerInfoSample := &smoothoperatorv1.OwnerInfo{} + Expect(readOwnerInfo(ownerInfoSample)).To(Succeed(), "Reading and parsing the Ownerinfo sample failed") + ownerInfo = ownerInfoSample.DeepCopy() + Expect(ownerInfo).NotTo(BeNil()) + Expect(createOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + }) AfterEach(func() { - // TODO (user): Add any teardown logic common to all tests + Expect(k8sClient.Delete(ctx, ownerInfo)).To(Succeed()) }) - Context("When creating WMS under Conversion Webhook", func() { - // TODO (user): Add logic to convert the object to the desired version and verify the conversion - // Example: - // It("Should convert the object correctly", func() { - // convertedObj := &pdoknlv3.WMS{} - // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) - // Expect(convertedObj).ToNot(BeNil()) - // }) + Context("When creating or updating WMS under Conversion Webhook", func() { + ctx := context.Background() + + It("Creates the WMS from the sample", func() { + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(BeEmpty()) + }) + + It("Should Deny Create when Labels are empty", func() { + obj.Labels = nil + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("metadata").Child("labels"), + "can't be empty", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when URL not in IngressRouteURLs", func() { + url, err := smoothoperatormodel.ParseURL("http://changed/changed") + Expect(err).To(BeNil()) + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} + url, err = smoothoperatormodel.ParseURL("http://sample/sample") + Expect(err).To(BeNil()) + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("must contain baseURL: %s", url), + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Warns when the name contains WMS", func() { + obj.Name += "-wms" + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("metadata").Child("name"), + "name should not contain wms", + []string{}, + ))) + }) + + It("Warns when mapfile and resolution are set", func() { + withMapfile(obj) + obj.Spec.Service.Resolution = ptr.To(int32(5)) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("resolution"), + "not used when service.mapfile is configured", + []string{}, + ))) + }) + + It("Warns when mapfile and defResolution are set", func() { + withMapfile(obj) + obj.Spec.Service.DefResolution = ptr.To(int32(5)) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("defResolution"), + "not used when service.mapfile is configured", + []string{}, + ))) + }) + + It("Should deny Create when URL not in IngressRouteURLs", func() { + obj.Spec.Service.Inspire = &pdoknlv3.Inspire{ServiceMetadataURL: pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}}} + obj.Spec.Service.Layer.Layers[0].DatasetMetadataURL = &pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("inspire").Child("csw").Child("metadataIdentifier"), + "metadata", + "serviceMetadataUrl.csw.metadataIdentifier cannot also be used as an datasetMetadataUrl.csw.metadataIdentifier", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when minReplicas are larger than maxReplicas", func() { + obj.Spec.HorizontalPodAutoscalerPatch = &pdoknlv3.HorizontalPodAutoscalerPatch{ + MinReplicas: ptr.To(int32(10)), + MaxReplicas: ptr.To(int32(5)), + } + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("horizontalPodAutoscaler"), + fmt.Sprintf("minReplicas: %d, maxReplicas: %d", 10, 5), + "maxReplicas cannot be less than minReplicas", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when mapserver container doesn't have ephemeral storage", func() { + obj.Spec.PodSpecPatch = corev1.PodSpec{} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required(field.NewPath("spec"). + Child("podSpecPatch"). + Child("containers"). + Key("mapserver"). + Child("resources"). + Child("limits"). + Child(corev1.ResourceEphemeralStorage.String()), "")))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when multiple layers have the same name", func() { + layerName := "equal" + obj.Spec.Service.Layer.Layers[0].Name = &layerName + obj.Spec.Service.Layer.Layers[1].Name = &layerName + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Duplicate( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("name"), + layerName, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when Group Layer has data set", func() { + data := pdoknlv3.Data{} + obj.Spec.Service.Layer.Layers[1].Data = &data + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("data"), + data, + "must not be set on a GroupLayer", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Warns when mapfile and layer boundingboxes are both set", func() { + withMapfile(obj) + obj.Spec.Service.Layer.BoundingBoxes = []pdoknlv3.WMSBoundingBox{{ + CRS: "", + BBox: smoothoperatormodel.BBox{}, + }} + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("boundingBoxes"), + "is not used when service.mapfile is configured", + []string{}, + ))) + }) + + It("Should deny Create when there is no layer boundingbox set for dataepsg and no custom mapfile", func() { + obj.Spec.Service.Mapfile = nil + obj.Spec.Service.DataEPSG = "EPSG:1234" + obj.Spec.Service.Layer.Layers = []pdoknlv3.Layer{obj.Spec.Service.Layer.Layers[0]} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("boundingBoxes").Child("crs"), + fmt.Sprintf("must contain a boundingBox for CRS %s when service.dataEPSG is not 'EPSG:28992'", obj.Spec.Service.DataEPSG), + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Warns when unused fields are set on a tiff connection when using a custom mapfile", func() { + withMapfile(obj) + obj.Spec.Service.Layer.Layers[0].Data = &pdoknlv3.Data{TIF: &pdoknlv3.TIF{ + BlobKey: "blobkey", + Resample: "AVERAGE", + Offsite: ptr.To("offsite"), + GetFeatureInfoIncludesClass: true, + }} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("data").Child("tif").Child("getFeatureInfoIncludesClass"), + "is not used when service.mapfile is configured", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("data").Child("tif").Child("offsite"), + "is not used when service.mapfile is configured", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("data").Child("tif").Child("resample"), + "is not used when service.mapfile is configured", + []string{}, + ))))) + }) + + It("Should deny Create when there is a Group Layer that is not visible", func() { + obj.Spec.Service.Layer.Layers[1].Visible = false + obj.Spec.Service.Layer.Layers[1].Title = nil + obj.Spec.Service.Layer.Layers[1].Abstract = nil + obj.Spec.Service.Layer.Layers[1].Keywords = nil + obj.Spec.Service.Layer.Layers[1].Styles[0].Title = nil + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("visible"), + false, + "must be true for a "+pdoknlv3.GroupLayer, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Warns when unused fields are set on a layer that is not visible", func() { + obj.Spec.Service.Layer.Layers[0].Visible = false + obj.Spec.Service.Layer.Layers[0].Title = ptr.To("title") + obj.Spec.Service.Layer.Layers[0].Abstract = ptr.To("abstract") + obj.Spec.Service.Layer.Layers[0].Keywords = []string{"keyword"} + obj.Spec.Service.Layer.Layers[0].DatasetMetadataURL = &pdoknlv3.MetadataURL{} + obj.Spec.Service.Layer.Layers[0].Authority = &pdoknlv3.Authority{} + obj.Spec.Service.Layer.Layers[0].Styles[0].Title = ptr.To("title") + obj.Spec.Service.Layer.Layers[0].Styles[0].Abstract = ptr.To("abstract") + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(Equal(getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("abstract"), + "is not used when layer.visible=false", getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("title"), + "is not used when layer.visible=false", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("authority"), + "is not used when layer.visible=false", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("datasetMetadataURL"), + "is not used when layer.visible=false", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("keywords"), + "is not used when layer.visible=false", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("abstract"), + "is not used when layer.visible=false", + getValidationWarnings( + obj, + *field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("title"), + "is not used when layer.visible=false", + []string{}, + ))))))))) + }) + + It("Should deny Create when a Layer has multiple boundingBoxes with the same CRS", func() { + bbox := pdoknlv3.WMSBoundingBox{ + CRS: "EPSG:28992", + BBox: smoothoperatormodel.BBox{ + MinX: "-25000", + MinY: "250000", + MaxX: "280000", + MaxY: "860000", + }, + } + obj.Spec.Service.Layer.BoundingBoxes = []pdoknlv3.WMSBoundingBox{bbox, bbox} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Duplicate( + field.NewPath("spec").Child("service").Child("layer").Child("boundingBoxes").Index(1).Child("crs"), + bbox.CRS, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a Layer uses the same style name multiple times", func() { + styleName := "duplicate" + style := pdoknlv3.Style{ + Name: styleName, + Title: ptr.To("Title"), + Visualization: obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization, + } + obj.Spec.Service.Layer.Layers[0].Styles = []pdoknlv3.Style{style, style} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(1).Child("name"), + styleName, + "A Layer can't use the same style name multiple times", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a Style doesn't have a title on its highest visible layer", func() { + obj.Spec.Service.Layer.Layers[1].Styles[0].Title = nil + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("styles").Index(0).Child("title"), + "A Style must have a title on the highest visible Layer", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a GroupLayer Style uses the same name as a Style from a parent Layer", func() { + styleName := "duplicate" + obj.Spec.Service.Layer.Styles = []pdoknlv3.Style{{Name: styleName, Title: ptr.To("title")}} + obj.Spec.Service.Layer.Layers[1].Styles = []pdoknlv3.Style{{Name: styleName, Title: ptr.To("title")}} + obj.Spec.Service.Layer.Layers[0].Styles[0].Name = styleName + obj.Spec.Service.Layer.Layers[1].Layers[0].Styles[0].Name = styleName + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("styles").Index(0).Child("name"), + styleName, + "A GroupLayer can't redefine the same style as a parent layer", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a GroupLayer Style has visualization", func() { + visualization := "file.style" + obj.Spec.Service.Layer.Layers[1].Styles[0].Visualization = &visualization + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("styles").Index(0).Child("visualization"), + visualization, + "GroupLayers must not have a visualization", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a Style has a visualization while a custom mapfile is configured", func() { + visualization := "file.style" + withMapfile(obj) + obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization = &visualization + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("visualization"), + visualization, + "is not used when spec.service.mapfile is used", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a Data Layer has a Style with no visualization while a no custom mapfile is configured", func() { + obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization = nil + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("visualization"), + "on DataLayers when spec.service.mapfile is not used", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a when a Visualization file is not defined in the stylingassets", func() { + visualization := "new.style" + obj.Spec.Service.Layer.Layers[0].Styles[0].Visualization = &visualization + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(0).Child("styles").Index(0).Child("visualization"), + visualization, + "must be defined be in spec.service.stylingAssets.configMapKeyRefs.Keys", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when a when a Group Layer style isn't implemented in a sub Data Layer", func() { + obj.Spec.Service.Layer.Layers[1].Styles[0].Name = "new" + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("service").Child("layer").Child("layers").Index(1).Child("layers").Index(0).Child("styles"), + nil, + fmt.Sprintf("dataLayer must implement style: %s, defined by a parent layer", "new"), + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny Create when there are no visible layers", func() { + obj.Spec.Service.Layer.Layers = []pdoknlv3.Layer{obj.Spec.Service.Layer.Layers[1].Layers[0]} + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("layer").Child("layers[*]").Child("visible"), + "at least one layer must be visible", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef doesn't exist", func() { + obj.Spec.Service.OwnerInfoRef = "changed" + + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.NotFound( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "changed", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef misses namespaceTemplate", func() { + ownerInfo.Spec.NamespaceTemplate = nil + + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.namespaceTemplate missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef misses csw metadataTemplate", func() { + obj.Spec.Service.Inspire = &pdoknlv3.Inspire{ServiceMetadataURL: pdoknlv3.MetadataURL{CSW: &pdoknlv3.Metadata{MetadataIdentifier: "metadata"}}} + ownerInfo.Spec.MetadataUrls.CSW = nil + + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.metadataUrls.csw missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + + ownerInfo.Spec.MetadataUrls = nil + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err = validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.metadataUrls.csw missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny create if the OwnerInfoRef misses WMS", func() { + ownerInfo.Spec.WMS = nil + Expect(updateOwnerInfo(ctx, k8sClient, ownerInfo)).To(Succeed()) + warnings, err := validator.ValidateCreate(ctx, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("spec").Child("service").Child("ownerInfoRef"), + "spec.WMS missing in "+ownerInfo.Name, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a ingressRouteURL was removed", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).To(BeNil()) + oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ + {URL: obj.URL()}, + {URL: smoothoperatormodel.URL{URL: url}}, + } + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: obj.URL()}} + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("urls cannot be removed, missing: %s", smoothoperatormodel.IngressRouteURL{URL: smoothoperatormodel.URL{URL: url}}), + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should accept update if a url was changed when it's in ingressRouteUrls", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).To(BeNil()) + oldObj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{ + {URL: obj.URL()}, + {URL: smoothoperatormodel.URL{URL: url}}, + } + obj.Spec.IngressRouteURLs = oldObj.Spec.IngressRouteURLs + oldObj.Spec.Service.URL = obj.URL() + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(BeNil()) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a url was changed and ingressRouteUrls = nil", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).To(BeNil()) + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + obj.Spec.IngressRouteURLs = nil + oldObj.Spec.IngressRouteURLs = nil + + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("spec").Child("service").Child("url"), + "is immutable, add the old and new urls to spec.ingressRouteUrls in order to change this field", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update url was changed but not added to ingressRouteURLs", func() { + url, err := smoothoperatormodel.ParseURL("http://new.url/path") + Expect(err).ToNot(HaveOccurred()) + oldObj.Spec.IngressRouteURLs = nil + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: oldObj.Spec.Service.URL}} + obj.Spec.Service.URL = smoothoperatormodel.URL{URL: url} + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("must contain baseURL: %s", obj.URL()), + )))) + Expect(warnings).To(BeEmpty()) + + obj.Spec.IngressRouteURLs = []smoothoperatormodel.IngressRouteURL{{URL: smoothoperatormodel.URL{URL: url}}} + warnings, err = validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("spec").Child("ingressRouteUrls"), + fmt.Sprint(obj.Spec.IngressRouteURLs), + fmt.Sprintf("must contain baseURL: %s", oldObj.URL()), + )))) + Expect(warnings).To(BeEmpty()) + + }) + + It("Should deny update if a label was removed", func() { + oldKey := "" + for label := range obj.Labels { + oldKey = label + delete(obj.Labels, label) + break + } + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Required( + field.NewPath("metadata").Child("labels").Child(oldKey), + "labels cannot be removed", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a label changed", func() { + oldKey := "" + oldValue := "" + newValue := "" + for label, val := range obj.Labels { + oldKey = label + oldValue = val + newValue = val + "-newval" + obj.Labels[label] = newValue + break + } + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Invalid( + field.NewPath("metadata").Child("labels").Child(oldKey), + newValue, + "immutable: should be: "+oldValue, + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if a label was added", func() { + newKey := "new-label" + obj.Labels[newKey] = "test" + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("metadata").Child("labels").Child(newKey), + "new labels cannot be added", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if an inspire block was added", func() { + obj.Spec.Service.Inspire = &pdoknlv3.Inspire{} + oldObj.Spec.Service.Inspire = nil + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("spec").Child("service").Child("inspire"), + "cannot change from inspire to not inspire or the other way around", + )))) + Expect(warnings).To(BeEmpty()) + }) + + It("Should deny update if an inspire block was removed", func() { + oldObj.Spec.Service.Inspire = &pdoknlv3.Inspire{} + obj.Spec.Service.Inspire = nil + warnings, err := validator.ValidateUpdate(ctx, oldObj, obj) + Expect(err).To(Equal(getValidationError(obj, field.Forbidden( + field.NewPath("spec").Child("service").Child("inspire"), + "cannot change from inspire to not inspire or the other way around", + )))) + Expect(warnings).To(BeEmpty()) + }) + }) }) + +func withMapfile(wms *pdoknlv3.WMS) { + wms.Spec.Service.Mapfile = &pdoknlv3.Mapfile{} + wms.Spec.Service.Layer.Layers[0].Styles[0].Visualization = nil + wms.Spec.Service.Layer.Layers[1].Layers[0].Styles[0].Visualization = nil +} diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index cfb69c0..ed2d505 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -16,6 +16,7 @@ limitations under the License. package e2e +//nolint:revive // Complains about the dot imports import ( "fmt" "os" @@ -30,10 +31,14 @@ import ( var ( // Optional Environment Variables: + // - PROMETHEUS_INSTALL_SKIP=true: Skips Prometheus Operator installation during test setup. // - CERT_MANAGER_INSTALL_SKIP=true: Skips CertManager installation during test setup. - // These variables are useful if CertManager is already installed, avoiding + // These variables are useful if Prometheus or CertManager is already installed, avoiding // re-installation and conflicts. + skipPrometheusInstall = os.Getenv("PROMETHEUS_INSTALL_SKIP") == "true" skipCertManagerInstall = os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" + // isPrometheusOperatorAlreadyInstalled will be set true when prometheus CRDs be found on the cluster + isPrometheusOperatorAlreadyInstalled = false // isCertManagerAlreadyInstalled will be set true when CertManager CRDs be found on the cluster isCertManagerAlreadyInstalled = false @@ -45,7 +50,7 @@ var ( // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, // temporary environment to validate project changes with the the purposed to be used in CI jobs. // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs -// CertManager. +// CertManager and Prometheus. func TestE2E(t *testing.T) { RegisterFailHandler(Fail) _, _ = fmt.Fprintf(GinkgoWriter, "Starting mapserver-operator integration test suite\n") @@ -53,8 +58,11 @@ func TestE2E(t *testing.T) { } var _ = BeforeSuite(func() { + By("Ensure that Prometheus is enabled") + _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") + By("building the manager(Operator) image") - cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", projectImage)) + cmd := exec.Command("make", "docker-build", "IMG="+projectImage) _, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager(Operator) image") @@ -65,9 +73,19 @@ var _ = BeforeSuite(func() { ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager(Operator) image into Kind") // The tests-e2e are intended to run on a temporary cluster that is created and destroyed for testing. - // To prevent errors when tests run in environments with CertManager already installed, - // we check for its presence before execution. - // Setup CertManager before the suite if not skipped and if not already installed + // To prevent errors when tests run in environments with Prometheus or CertManager already installed, + // we check for their presence before execution. + // Setup Prometheus and CertManager before the suite if not skipped and if not already installed + if !skipPrometheusInstall { + By("checking if prometheus is installed already") + isPrometheusOperatorAlreadyInstalled = utils.IsPrometheusCRDsInstalled() + if !isPrometheusOperatorAlreadyInstalled { + _, _ = fmt.Fprintf(GinkgoWriter, "Installing Prometheus Operator...\n") + Expect(utils.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") + } else { + _, _ = fmt.Fprintf(GinkgoWriter, "WARNING: Prometheus Operator is already installed. Skipping installation...\n") + } + } if !skipCertManagerInstall { By("checking if cert manager is installed already") isCertManagerAlreadyInstalled = utils.IsCertManagerCRDsInstalled() @@ -81,7 +99,11 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - // Teardown CertManager after the suite if not skipped and if it was not already installed + // Teardown Prometheus and CertManager after the suite if not skipped and if they were not already installed + if !skipPrometheusInstall && !isPrometheusOperatorAlreadyInstalled { + _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling Prometheus Operator...\n") + utils.UninstallPrometheusOperator() + } if !skipCertManagerInstall && !isCertManagerAlreadyInstalled { _, _ = fmt.Fprintf(GinkgoWriter, "Uninstalling CertManager...\n") utils.UninstallCertManager() diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index e46a748..9989296 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -16,12 +16,10 @@ limitations under the License. package e2e +//nolint:revive // Complains about the dot imports import ( - "encoding/json" "fmt" - "os" "os/exec" - "path/filepath" "time" . "github.com/onsi/ginkgo/v2" @@ -33,15 +31,6 @@ import ( // namespace where the project is deployed in const namespace = "mapserver-operator-system" -// serviceAccountName created for the project -const serviceAccountName = "mapserver-operator-controller-manager" - -// metricsServiceName is the name of the metrics service of the project -const metricsServiceName = "mapserver-operator-controller-manager-metrics-service" - -// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data -const metricsRoleBindingName = "mapserver-operator-metrics-binding" - var _ = Describe("Manager", Ordered, func() { var controllerPodName string @@ -66,7 +55,7 @@ var _ = Describe("Manager", Ordered, func() { Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") By("deploying the controller-manager") - cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", projectImage)) + cmd = exec.Command("make", "deploy", "IMG="+projectImage) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") }) @@ -169,213 +158,5 @@ var _ = Describe("Manager", Ordered, func() { } Eventually(verifyControllerUp).Should(Succeed()) }) - - It("should ensure the metrics endpoint is serving metrics", func() { - By("creating a ClusterRoleBinding for the service account to allow access to metrics") - cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, - "--clusterrole=mapserver-operator-metrics-reader", - fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), - ) - _, err := utils.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") - - By("validating that the metrics service is available") - cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) - _, err = utils.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") - - By("getting the service account token") - token, err := serviceAccountToken() - Expect(err).NotTo(HaveOccurred()) - Expect(token).NotTo(BeEmpty()) - - By("waiting for the metrics endpoint to be ready") - verifyMetricsEndpointReady := func(g Gomega) { - cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) - output, err := utils.Run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready") - } - Eventually(verifyMetricsEndpointReady).Should(Succeed()) - - By("verifying that the controller manager is serving the metrics server") - verifyMetricsServerStarted := func(g Gomega) { - cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) - output, err := utils.Run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), - "Metrics server not yet started") - } - Eventually(verifyMetricsServerStarted).Should(Succeed()) - - By("creating the curl-metrics pod to access the metrics endpoint") - cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", - "--namespace", namespace, - "--image=curlimages/curl:latest", - "--overrides", - fmt.Sprintf(`{ - "spec": { - "containers": [{ - "name": "curl", - "image": "curlimages/curl:latest", - "command": ["/bin/sh", "-c"], - "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], - "securityContext": { - "allowPrivilegeEscalation": false, - "capabilities": { - "drop": ["ALL"] - }, - "runAsNonRoot": true, - "runAsUser": 1000, - "seccompProfile": { - "type": "RuntimeDefault" - } - } - }], - "serviceAccount": "%s" - } - }`, token, metricsServiceName, namespace, serviceAccountName)) - _, err = utils.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") - - By("waiting for the curl-metrics pod to complete.") - verifyCurlUp := func(g Gomega) { - cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", - "-o", "jsonpath={.status.phase}", - "-n", namespace) - output, err := utils.Run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") - } - Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) - - By("getting the metrics by checking curl-metrics logs") - metricsOutput := getMetricsOutput() - Expect(metricsOutput).To(ContainSubstring( - "controller_runtime_reconcile_total", - )) - }) - - It("should provisioned cert-manager", func() { - By("validating that cert-manager has the certificate Secret") - verifyCertManager := func(g Gomega) { - cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) - _, err := utils.Run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - } - Eventually(verifyCertManager).Should(Succeed()) - }) - - It("should have CA injection for WMS conversion webhook", func() { - By("checking CA injection for WMS conversion webhook") - verifyCAInjection := func(g Gomega) { - cmd := exec.Command("kubectl", "get", - "customresourcedefinitions.apiextensions.k8s.io", - "wms..pdok.nl", - "-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}") - vwhOutput, err := utils.Run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) - } - Eventually(verifyCAInjection).Should(Succeed()) - }) - - It("should have CA injection for validating webhooks", func() { - By("checking CA injection for validating webhooks") - verifyCAInjection := func(g Gomega) { - cmd := exec.Command("kubectl", "get", - "validatingwebhookconfigurations.admissionregistration.k8s.io", - "mapserver-operator-validating-webhook-configuration", - "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") - vwhOutput, err := utils.Run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) - } - Eventually(verifyCAInjection).Should(Succeed()) - }) - - It("should have CA injection for WFS conversion webhook", func() { - By("checking CA injection for WFS conversion webhook") - verifyCAInjection := func(g Gomega) { - cmd := exec.Command("kubectl", "get", - "customresourcedefinitions.apiextensions.k8s.io", - "wfs..pdok.nl", - "-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}") - vwhOutput, err := utils.Run(cmd) - g.Expect(err).NotTo(HaveOccurred()) - g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) - } - Eventually(verifyCAInjection).Should(Succeed()) - }) - - // +kubebuilder:scaffold:e2e-webhooks-checks - - // TODO: Customize the e2e test suite with scenarios specific to your project. - // Consider applying sample/CR(s) and check their status and/or verifying - // the reconciliation by using the metrics, i.e.: - // metricsOutput := getMetricsOutput() - // Expect(metricsOutput).To(ContainSubstring( - // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, - // strings.ToLower(), - // )) }) }) - -// serviceAccountToken returns a token for the specified service account in the given namespace. -// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request -// and parsing the resulting token from the API response. -func serviceAccountToken() (string, error) { - const tokenRequestRawString = `{ - "apiVersion": "authentication.k8s.io/v1", - "kind": "TokenRequest" - }` - - // Temporary file to store the token request - secretName := fmt.Sprintf("%s-token-request", serviceAccountName) - tokenRequestFile := filepath.Join("/tmp", secretName) - err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) - if err != nil { - return "", err - } - - var out string - verifyTokenCreation := func(g Gomega) { - // Execute kubectl command to create the token - cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( - "/api/v1/namespaces/%s/serviceaccounts/%s/token", - namespace, - serviceAccountName, - ), "-f", tokenRequestFile) - - output, err := cmd.CombinedOutput() - g.Expect(err).NotTo(HaveOccurred()) - - // Parse the JSON output to extract the token - var token tokenRequest - err = json.Unmarshal(output, &token) - g.Expect(err).NotTo(HaveOccurred()) - - out = token.Status.Token - } - Eventually(verifyTokenCreation).Should(Succeed()) - - return out, err -} - -// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. -func getMetricsOutput() string { - By("getting the curl-metrics logs") - cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) - metricsOutput, err := utils.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") - Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) - return metricsOutput -} - -// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, -// containing only the token field that we need to extract. -type tokenRequest struct { - Status struct { - Token string `json:"token"` - } `json:"status"` -} diff --git a/test/utils/utils.go b/test/utils/utils.go index 04a5141..fef95da 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -54,7 +54,7 @@ func Run(cmd *exec.Cmd) (string, error) { _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) output, err := cmd.CombinedOutput() if err != nil { - return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + return string(output), fmt.Errorf("%s failed with error: (%w) %s", command, err, string(output)) } return string(output), nil @@ -197,7 +197,7 @@ func GetProjectDir() (string, error) { if err != nil { return wd, err } - wd = strings.Replace(wd, "/test/e2e", "", -1) + wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } @@ -205,7 +205,6 @@ func GetProjectDir() (string, error) { // of the target content. The target content may span multiple lines. func UncommentCode(filename, target, prefix string) error { // false positive - // nolint:gosec content, err := os.ReadFile(filename) if err != nil { return err @@ -246,6 +245,6 @@ func UncommentCode(filename, target, prefix string) error { return err } // false positive - // nolint:gosec + //nolint:gosec return os.WriteFile(filename, out.Bytes(), 0644) } From 6dcd3b0546c3c1c1c764b1c91e46c39afe0420bb Mon Sep 17 00:00:00 2001 From: Jelle Dijkstra Date: Mon, 22 Dec 2025 16:08:32 +0100 Subject: [PATCH 3/7] (chore) initial scaffold from release version: v4.10.1 --- .devcontainer/devcontainer.json | 2 +- .devcontainer/post-install.sh | 6 +- .dockerignore | 12 +- .github/workflows/lint.yml | 4 +- .github/workflows/test-e2e.yml | 5 +- .gitignore | 3 + .golangci.yml | 57 +++---- Dockerfile | 10 +- Makefile | 94 +++++++---- PROJECT | 1 + README.md | 4 +- api/v2beta1/wfs_conversion.go | 12 ++ api/v2beta1/wfs_types.go | 48 ++++-- api/v2beta1/wms_conversion.go | 12 ++ api/v2beta1/wms_types.go | 48 ++++-- api/v2beta1/zz_generated.deepcopy.go | 33 +++- api/v3/wfs_types.go | 49 ++++-- api/v3/wms_types.go | 49 ++++-- api/v3/zz_generated.deepcopy.go | 33 +++- cmd/main.go | 74 ++------- config/crd/bases/pdok.nl_wfses.yaml | 162 +++++++++++++++++-- config/crd/bases/pdok.nl_wmses.yaml | 162 +++++++++++++++++-- config/default/kustomization.yaml | 210 ++++++++++++------------- config/manager/manager.yaml | 1 + config/rbac/kustomization.yaml | 2 +- go.mod | 104 ++++++------- go.sum | 214 ++++++++++++++------------ internal/controller/wfs_controller.go | 6 +- internal/controller/wms_controller.go | 6 +- internal/webhook/v3/wfs_webhook.go | 7 +- test/e2e/e2e_suite_test.go | 5 +- test/e2e/e2e_test.go | 55 ++++--- test/utils/utils.go | 111 ++++++------- 33 files changed, 1040 insertions(+), 561 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0e0eed2..a3ab754 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Kubebuilder DevContainer", - "image": "docker.io/golang:1.23", + "image": "golang:1.24", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {}, "ghcr.io/devcontainers/features/git:1": {} diff --git a/.devcontainer/post-install.sh b/.devcontainer/post-install.sh index 265c43e..67f3e97 100644 --- a/.devcontainer/post-install.sh +++ b/.devcontainer/post-install.sh @@ -1,16 +1,16 @@ #!/bin/bash set -x -curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 +curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind mv ./kind /usr/local/bin/kind -curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/amd64 +curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/linux/$(go env GOARCH) chmod +x kubebuilder mv kubebuilder /usr/local/bin/ KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt) -curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" +curl -LO "https://dl.k8s.io/release/$KUBECTL_VERSION/bin/linux/$(go env GOARCH)/kubectl" chmod +x kubectl mv kubectl /usr/local/bin/kubectl diff --git a/.dockerignore b/.dockerignore index a3aab7a..9af8280 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,11 @@ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file -# Ignore build and test binaries. -bin/ +# Ignore everything by default and re-include only needed files +** + +# Re-include Go source files (but not *_test.go) +!**/*.go +**/*_test.go + +# Re-include Go module files +!go.mod +!go.sum diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4951e33..4838c54 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,6 +18,6 @@ jobs: go-version-file: go.mod - name: Run linter - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 with: - version: v1.63.4 + version: v2.5.0 diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index b2eda8c..4cdfb30 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -19,16 +19,13 @@ jobs: - name: Install the latest version of kind run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 + curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - - name: Create kind cluster - run: kind create cluster - - name: Running Test e2e run: | go mod tidy diff --git a/.gitignore b/.gitignore index ada68ff..9f0f3a1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ go.work *.swp *.swo *~ + +# Kubeconfig might contain secrets +*.kubeconfig diff --git a/.golangci.yml b/.golangci.yml index 6b29746..e5b21b0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,33 +1,15 @@ +version: "2" run: - timeout: 5m allow-parallel-runners: true - -issues: - # don't skip warning about doc comments - # don't exclude the default set of lint - exclude-use-default: false - # restore some of the defaults - # (fill in the rest as needed) - exclude-rules: - - path: "api/*" - linters: - - lll - - path: "internal/*" - linters: - - dupl - - lll linters: - disable-all: true + default: none enable: + - copyloopvar - dupl - errcheck - - copyloopvar - ginkgolinter - goconst - gocyclo - - gofmt - - goimports - - gosimple - govet - ineffassign - lll @@ -36,12 +18,35 @@ linters: - prealloc - revive - staticcheck - - typecheck - unconvert - unparam - unused - -linters-settings: - revive: + settings: + revive: + rules: + - name: comment-spacings + - name: import-shadowing + exclusions: + generated: lax rules: - - name: comment-spacings + - linters: + - lll + path: api/* + - linters: + - dupl + - lll + path: internal/* + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile index 348b837..6466c48 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM docker.io/golang:1.23 AS builder +FROM golang:1.24 AS builder ARG TARGETOS ARG TARGETARCH @@ -11,13 +11,11 @@ COPY go.sum go.sum # and so that source changes don't invalidate our downloaded layer RUN go mod download -# Copy the go source -COPY cmd/main.go cmd/main.go -COPY api/ api/ -COPY internal/ internal/ +# Copy the Go source (relies on .dockerignore to filter) +COPY . . # Build -# the GOARCH has not a default value to allow the binary be built according to the host where the command +# the GOARCH has no default value to allow the binary to be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. diff --git a/Makefile b/Makefile index 9a4ab3b..04f7526 100644 --- a/Makefile +++ b/Makefile @@ -43,11 +43,11 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. - $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. @@ -59,35 +59,48 @@ vet: ## Run go vet against code. .PHONY: test test: manifests generate fmt vet setup-envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. # CertManager is installed by default; skip with: # - CERT_MANAGER_INSTALL_SKIP=true -.PHONY: test-e2e -test-e2e: manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. - @command -v kind >/dev/null 2>&1 || { \ +KIND_CLUSTER ?= mapserver-operator-test-e2e + +.PHONY: setup-test-e2e +setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist + @command -v $(KIND) >/dev/null 2>&1 || { \ echo "Kind is not installed. Please install Kind manually."; \ exit 1; \ } - @kind get clusters | grep -q 'kind' || { \ - echo "No Kind cluster is running. Please start a Kind cluster before running the e2e tests."; \ - exit 1; \ - } - go test ./test/e2e/ -v -ginkgo.v + @case "$$($(KIND) get clusters)" in \ + *"$(KIND_CLUSTER)"*) \ + echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ + *) \ + echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ + $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ + esac + +.PHONY: test-e2e +test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. + KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v + $(MAKE) cleanup-test-e2e + +.PHONY: cleanup-test-e2e +cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests + @$(KIND) delete cluster --name $(KIND_CLUSTER) .PHONY: lint lint: golangci-lint ## Run golangci-lint linter - $(GOLANGCI_LINT) run + "$(GOLANGCI_LINT)" run .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes - $(GOLANGCI_LINT) run --fix + "$(GOLANGCI_LINT)" run --fix .PHONY: lint-config lint-config: golangci-lint ## Verify golangci-lint linter configuration - $(GOLANGCI_LINT) config verify + "$(GOLANGCI_LINT)" config verify ##@ Build @@ -130,8 +143,8 @@ docker-buildx: ## Build and push docker image for the manager for cross-platform .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default > dist/install.yaml + cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} + "$(KUSTOMIZE)" build config/default > dist/install.yaml ##@ Deployment @@ -141,44 +154,53 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - + @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ + if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ + if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - + cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} + "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - + "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f - ##@ Dependencies ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): - mkdir -p $(LOCALBIN) + mkdir -p "$(LOCALBIN)" ## Tool Binaries KUBECTL ?= kubectl +KIND ?= kind KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions -KUSTOMIZE_VERSION ?= v5.5.0 -CONTROLLER_TOOLS_VERSION ?= v0.17.2 +KUSTOMIZE_VERSION ?= v5.7.1 +CONTROLLER_TOOLS_VERSION ?= v0.19.0 + #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) -ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') +ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \ + [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \ + printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/') + #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) -ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') -GOLANGCI_LINT_VERSION ?= v1.63.4 +ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \ + [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \ + printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/') +GOLANGCI_LINT_VERSION ?= v2.5.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. $(KUSTOMIZE): $(LOCALBIN) @@ -192,7 +214,7 @@ $(CONTROLLER_GEN): $(LOCALBIN) .PHONY: setup-envtest setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." - @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \ + @"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \ echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ exit 1; \ } @@ -205,20 +227,24 @@ $(ENVTEST): $(LOCALBIN) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) - $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed # $3 - specific version of package define go-install-tool -@[ -f "$(1)-$(3)" ] || { \ +@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ set -e; \ package=$(2)@$(3) ;\ echo "Downloading $${package}" ;\ -rm -f $(1) || true ;\ -GOBIN=$(LOCALBIN) go install $${package} ;\ -mv $(1) $(1)-$(3) ;\ +rm -f "$(1)" ;\ +GOBIN="$(LOCALBIN)" go install $${package} ;\ +mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\ } ;\ -ln -sf $(1)-$(3) $(1) +ln -sf "$$(realpath "$(1)-$(3)")" "$(1)" +endef + +define gomodver +$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null) endef diff --git a/PROJECT b/PROJECT index 55b9919..2c555d1 100644 --- a/PROJECT +++ b/PROJECT @@ -2,6 +2,7 @@ # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html +cliVersion: 4.10.1 domain: pdok.nl layout: - go.kubebuilder.io/v4 diff --git a/README.md b/README.md index e75fcab..bee2a3a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## Getting Started ### Prerequisites -- go version v1.23.0+ +- go version v1.24.6+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. @@ -97,7 +97,7 @@ kubectl apply -f https://raw.githubusercontent.com//mapserver-operator/ 0 { setupLog.Info("Initializing webhook certificate watcher using provided certificates", "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) - var err error - webhookCertWatcher, err = certwatcher.New( - filepath.Join(webhookCertPath, webhookCertName), - filepath.Join(webhookCertPath, webhookCertKey), - ) - if err != nil { - setupLog.Error(err, "Failed to initialize webhook certificate watcher") - os.Exit(1) - } - - webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { - config.GetCertificate = webhookCertWatcher.GetCertificate - }) + webhookServerOptions.CertDir = webhookCertPath + webhookServerOptions.CertName = webhookCertName + webhookServerOptions.KeyName = webhookCertKey } - webhookServer := webhook.NewServer(webhook.Options{ - TLSOpts: webhookTLSOpts, - }) + webhookServer := webhook.NewServer(webhookServerOptions) // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/server + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.4/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, @@ -150,7 +136,7 @@ func main() { // FilterProvider is used to protect the metrics endpoint with authn/authz. // These configurations ensure that only authorized users and service accounts // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/metrics/filters#WithAuthenticationAndAuthorization + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.4/pkg/metrics/filters#WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } @@ -166,19 +152,9 @@ func main() { setupLog.Info("Initializing metrics certificate watcher using provided certificates", "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) - var err error - metricsCertWatcher, err = certwatcher.New( - filepath.Join(metricsCertPath, metricsCertName), - filepath.Join(metricsCertPath, metricsCertKey), - ) - if err != nil { - setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) - os.Exit(1) - } - - metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { - config.GetCertificate = metricsCertWatcher.GetCertificate - }) + metricsServerOptions.CertDir = metricsCertPath + metricsServerOptions.CertName = metricsCertName + metricsServerOptions.KeyName = metricsCertKey } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ @@ -205,14 +181,14 @@ func main() { os.Exit(1) } - if err = (&controller.WMSReconciler{ + if err := (&controller.WMSReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "WMS") os.Exit(1) } - if err = (&controller.WFSReconciler{ + if err := (&controller.WFSReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { @@ -221,36 +197,20 @@ func main() { } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = webhookpdoknlv3.SetupWMSWebhookWithManager(mgr); err != nil { + if err := webhookv3.SetupWMSWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "WMS") os.Exit(1) } } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { - if err = webhookpdoknlv3.SetupWFSWebhookWithManager(mgr); err != nil { + if err := webhookv3.SetupWFSWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "WFS") os.Exit(1) } } // +kubebuilder:scaffold:builder - if metricsCertWatcher != nil { - setupLog.Info("Adding metrics certificate watcher to manager") - if err := mgr.Add(metricsCertWatcher); err != nil { - setupLog.Error(err, "unable to add metrics certificate watcher to manager") - os.Exit(1) - } - } - - if webhookCertWatcher != nil { - setupLog.Info("Adding webhook certificate watcher to manager") - if err := mgr.Add(webhookCertWatcher); err != nil { - setupLog.Error(err, "unable to add webhook certificate watcher to manager") - os.Exit(1) - } - } - if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) diff --git a/config/crd/bases/pdok.nl_wfses.yaml b/config/crd/bases/pdok.nl_wfses.yaml index 4d8ef84..479bc2b 100644 --- a/config/crd/bases/pdok.nl_wfses.yaml +++ b/config/crd/bases/pdok.nl_wfses.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: wfses.pdok.nl spec: group: pdok.nl @@ -17,7 +17,7 @@ spec: - name: v2beta1 schema: openAPIV3Schema: - description: WFS is the Schema for the wfs API. + description: WFS is the Schema for the wfs API properties: apiVersion: description: |- @@ -37,16 +37,88 @@ spec: metadata: type: object spec: - description: WFSSpec defines the desired state of WFS. + description: spec defines the desired state of WFS properties: foo: - description: Foo is an example field of WFS. Edit wfs_types.go to + description: foo is an example field of WFS. Edit wfs_types.go to remove/update type: string type: object status: - description: WFSStatus defines the observed state of WFS. + description: status defines the observed state of WFS + properties: + conditions: + description: |- + conditions represent the current state of the WFS resource. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types include: + - "Available": the resource is fully functional + - "Progressing": the resource is being created or updated + - "Degraded": the resource failed to reach or maintain its desired state + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map type: object + required: + - spec type: object served: true storage: false @@ -55,7 +127,7 @@ spec: - name: v3 schema: openAPIV3Schema: - description: WFS is the Schema for the wfs API. + description: WFS is the Schema for the wfs API properties: apiVersion: description: |- @@ -75,16 +147,88 @@ spec: metadata: type: object spec: - description: WFSSpec defines the desired state of WFS. + description: spec defines the desired state of WFS properties: foo: - description: Foo is an example field of WFS. Edit wfs_types.go to + description: foo is an example field of WFS. Edit wfs_types.go to remove/update type: string type: object status: - description: WFSStatus defines the observed state of WFS. + description: status defines the observed state of WFS + properties: + conditions: + description: |- + conditions represent the current state of the WFS resource. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types include: + - "Available": the resource is fully functional + - "Progressing": the resource is being created or updated + - "Degraded": the resource failed to reach or maintain its desired state + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map type: object + required: + - spec type: object served: true storage: true diff --git a/config/crd/bases/pdok.nl_wmses.yaml b/config/crd/bases/pdok.nl_wmses.yaml index 7d3ebcb..98a0d2c 100644 --- a/config/crd/bases/pdok.nl_wmses.yaml +++ b/config/crd/bases/pdok.nl_wmses.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.17.2 + controller-gen.kubebuilder.io/version: v0.19.0 name: wmses.pdok.nl spec: group: pdok.nl @@ -17,7 +17,7 @@ spec: - name: v2beta1 schema: openAPIV3Schema: - description: WMS is the Schema for the wms API. + description: WMS is the Schema for the wms API properties: apiVersion: description: |- @@ -37,16 +37,88 @@ spec: metadata: type: object spec: - description: WMSSpec defines the desired state of WMS. + description: spec defines the desired state of WMS properties: foo: - description: Foo is an example field of WMS. Edit wms_types.go to + description: foo is an example field of WMS. Edit wms_types.go to remove/update type: string type: object status: - description: WMSStatus defines the observed state of WMS. + description: status defines the observed state of WMS + properties: + conditions: + description: |- + conditions represent the current state of the WMS resource. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types include: + - "Available": the resource is fully functional + - "Progressing": the resource is being created or updated + - "Degraded": the resource failed to reach or maintain its desired state + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map type: object + required: + - spec type: object served: true storage: false @@ -55,7 +127,7 @@ spec: - name: v3 schema: openAPIV3Schema: - description: WMS is the Schema for the wms API. + description: WMS is the Schema for the wms API properties: apiVersion: description: |- @@ -75,16 +147,88 @@ spec: metadata: type: object spec: - description: WMSSpec defines the desired state of WMS. + description: spec defines the desired state of WMS properties: foo: - description: Foo is an example field of WMS. Edit wms_types.go to + description: foo is an example field of WMS. Edit wms_types.go to remove/update type: string type: object status: - description: WMSStatus defines the observed state of WMS. + description: status defines the observed state of WMS + properties: + conditions: + description: |- + conditions represent the current state of the WMS resource. + Each condition has a unique type and reflects the status of a specific aspect of the resource. + + Standard condition types include: + - "Available": the resource is fully functional + - "Progressing": the resource is being created or updated + - "Degraded": the resource failed to reach or maintain its desired state + + The status of each condition is one of True, False, or Unknown. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map type: object + required: + - spec type: object served: true storage: true diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index be72a73..97ab442 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -22,7 +22,7 @@ resources: # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. @@ -56,7 +56,7 @@ patches: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: +replacements: # - source: # Uncomment the following block to enable certificates for metrics # kind: Service # version: v1 @@ -86,7 +86,7 @@ patches: # delimiter: '.' # index: 0 # create: true -# + # - source: # kind: Service # version: v1 @@ -116,75 +116,75 @@ patches: # delimiter: '.' # index: 1 # create: true -# -# - source: # Uncomment the following block if you have any webhook -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # Name of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # Namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true -# -# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true -# + + - source: # Uncomment the following block if you have any webhook + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.name # Name of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 0 + create: true + - source: + kind: Service + version: v1 + name: webhook-service + fieldPath: .metadata.namespace # Namespace of the service + targets: + - select: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPaths: + - .spec.dnsNames.0 + - .spec.dnsNames.1 + options: + delimiter: '.' + index: 1 + create: true + + - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert # This name should match the one in certificate.yaml + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: + - select: + kind: ValidatingWebhookConfiguration + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true + # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) # kind: Certificate # group: cert-manager.io @@ -215,23 +215,23 @@ patches: # delimiter: '/' # index: 1 # create: true -# -# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# - select: -# kind: CustomResourceDefinition -# name: wms.pdok.nl -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true + + - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.namespace # Namespace of the certificate CR + targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - select: + kind: CustomResourceDefinition + name: wms.pdok.nl + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 0 + create: true # - select: # kind: CustomResourceDefinition # name: wfs.pdok.nl @@ -242,22 +242,22 @@ patches: # index: 0 # create: true # +kubebuilder:scaffold:crdkustomizecainjectionns -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# - select: -# kind: CustomResourceDefinition -# name: wms.pdok.nl -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true + - source: + kind: Certificate + group: cert-manager.io + version: v1 + name: serving-cert + fieldPath: .metadata.name + targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. + - select: + kind: CustomResourceDefinition + name: wms.pdok.nl + fieldPaths: + - .metadata.annotations.[cert-manager.io/inject-ca-from] + options: + delimiter: '/' + index: 1 + create: true # - select: # kind: CustomResourceDefinition # name: wfs.pdok.nl diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 2e94912..596a2a6 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -67,6 +67,7 @@ spec: name: manager ports: [] securityContext: + readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 06d35b7..55cda0b 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -20,7 +20,7 @@ resources: - metrics_reader_role.yaml # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by # default, aiding admins in cluster management. Those roles are -# not used by the {{ .ProjectName }} itself. You can comment the following lines +# not used by the mapserver-operator itself. You can comment the following lines # if you do not want those helpers be installed with your Project. - wfs_admin_role.yaml - wfs_editor_role.yaml diff --git a/go.mod b/go.mod index 665a78d..1bcf187 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,28 @@ module github.com/pdok/mapserver-operator -go 1.23.0 - -godebug default=go1.23 +go 1.24.6 require ( github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 - k8s.io/apimachinery v0.32.1 - k8s.io/client-go v0.32.1 - sigs.k8s.io/controller-runtime v0.20.2 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + sigs.k8s.io/controller-runtime v0.22.4 ) require ( - cel.dev/expr v0.18.0 // indirect + cel.dev/expr v0.24.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect @@ -34,67 +31,70 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.22.0 // indirect - github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect - go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/term v0.25.0 // indirect - golang.org/x/text v0.19.0 // indirect - golang.org/x/time v0.7.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.26.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect - google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.1 // indirect - k8s.io/apiextensions-apiserver v0.32.1 // indirect - k8s.io/apiserver v0.32.1 // indirect - k8s.io/component-base v0.32.1 // indirect + k8s.io/api v0.34.1 // indirect + k8s.io/apiextensions-apiserver v0.34.1 // indirect + k8s.io/apiserver v0.34.1 // indirect + k8s.io/component-base v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index b257e61..3797258 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,7 @@ -cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= -cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -12,24 +10,23 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -53,13 +50,12 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.22.0 h1:b3FJZxpiv1vTMo2/5RDUqAHPxkT8mmMfJIrq1llbf7g= -github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -67,8 +63,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -77,6 +73,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -84,13 +82,16 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= @@ -99,61 +100,70 @@ github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= -github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -165,28 +175,28 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= -golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -199,14 +209,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw= -google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -217,31 +227,33 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc= -k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k= -k8s.io/apiextensions-apiserver v0.32.1 h1:hjkALhRUeCariC8DiVmb5jj0VjIc1N0DREP32+6UXZw= -k8s.io/apiextensions-apiserver v0.32.1/go.mod h1:sxWIGuGiYov7Io1fAS2X06NjMIk5CbRHc2StSmbaQto= -k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs= -k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/apiserver v0.32.1 h1:oo0OozRos66WFq87Zc5tclUX2r0mymoVHRq8JmR7Aak= -k8s.io/apiserver v0.32.1/go.mod h1:UcB9tWjBY7aryeI5zAgzVJB/6k7E97bkr1RgqDz0jPw= -k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU= -k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg= -k8s.io/component-base v0.32.1 h1:/5IfJ0dHIKBWysGV0yKTFfacZ5yNV1sulPh3ilJjRZk= -k8s.io/component-base v0.32.1/go.mod h1:j1iMMHi/sqAHeG5z+O9BFNCF698a1u0186zkjMZQ28w= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc= -sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/controller/wfs_controller.go b/internal/controller/wfs_controller.go index b2187f9..c6c9c64 100644 --- a/internal/controller/wfs_controller.go +++ b/internal/controller/wfs_controller.go @@ -22,7 +22,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + logf "sigs.k8s.io/controller-runtime/pkg/log" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) @@ -45,9 +45,9 @@ type WFSReconciler struct { // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.4/pkg/reconcile func (r *WFSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + _ = logf.FromContext(ctx) // TODO(user): your logic here diff --git a/internal/controller/wms_controller.go b/internal/controller/wms_controller.go index 0558be9..83de911 100644 --- a/internal/controller/wms_controller.go +++ b/internal/controller/wms_controller.go @@ -22,7 +22,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + logf "sigs.k8s.io/controller-runtime/pkg/log" pdoknlv3 "github.com/pdok/mapserver-operator/api/v3" ) @@ -45,9 +45,9 @@ type WMSReconciler struct { // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.22.4/pkg/reconcile func (r *WMSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + _ = logf.FromContext(ctx) // TODO(user): your logic here diff --git a/internal/webhook/v3/wfs_webhook.go b/internal/webhook/v3/wfs_webhook.go index ccbfca4..0726cd5 100644 --- a/internal/webhook/v3/wfs_webhook.go +++ b/internal/webhook/v3/wfs_webhook.go @@ -43,8 +43,7 @@ func SetupWFSWebhookWithManager(mgr ctrl.Manager) error { // TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. -// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'. // +kubebuilder:webhook:path=/validate-pdok-nl-v3-wfs,mutating=false,failurePolicy=fail,sideEffects=None,groups=pdok.nl,resources=wfs,verbs=create;update,versions=v3,name=vwfs-v3.kb.io,admissionReviewVersions=v1 // WFSCustomValidator struct is responsible for validating the WFS resource @@ -59,7 +58,7 @@ type WFSCustomValidator struct { var _ webhook.CustomValidator = &WFSCustomValidator{} // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { wfs, ok := obj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object but got %T", obj) @@ -72,7 +71,7 @@ func (v *WFSCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Obj } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type WFS. -func (v *WFSCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (v *WFSCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { wfs, ok := newObj.(*pdoknlv3.WFS) if !ok { return nil, fmt.Errorf("expected a WFS object for the newObj but got %T", newObj) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index cfb69c0..45e9263 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -1,3 +1,6 @@ +//go:build e2e +// +build e2e + /* Copyright 2025. @@ -43,7 +46,7 @@ var ( ) // TestE2E runs the end-to-end (e2e) test suite for the project. These tests execute in an isolated, -// temporary environment to validate project changes with the the purposed to be used in CI jobs. +// temporary environment to validate project changes with the purpose of being used in CI jobs. // The default setup requires Kind, builds/loads the Manager Docker image locally, and installs // CertManager. func TestE2E(t *testing.T) { diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index e46a748..32422ce 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -1,3 +1,6 @@ +//go:build e2e +// +build e2e + /* Copyright 2025. @@ -189,24 +192,38 @@ var _ = Describe("Manager", Ordered, func() { Expect(err).NotTo(HaveOccurred()) Expect(token).NotTo(BeEmpty()) - By("waiting for the metrics endpoint to be ready") - verifyMetricsEndpointReady := func(g Gomega) { - cmd := exec.Command("kubectl", "get", "endpoints", metricsServiceName, "-n", namespace) + By("ensuring the controller pod is ready") + verifyControllerPodReady := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace, + "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("8443"), "Metrics endpoint is not ready") + g.Expect(output).To(Equal("True"), "Controller pod not ready") } - Eventually(verifyMetricsEndpointReady).Should(Succeed()) + Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying that the controller manager is serving the metrics server") verifyMetricsServerStarted := func(g Gomega) { cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(output).To(ContainSubstring("controller-runtime.metrics\tServing metrics server"), + g.Expect(output).To(ContainSubstring("Serving metrics server"), "Metrics server not yet started") } - Eventually(verifyMetricsServerStarted).Should(Succeed()) + Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed()) + + By("waiting for the webhook service endpoints to be ready") + verifyWebhookEndpointsReady := func(g Gomega) { + cmd := exec.Command("kubectl", "get", "endpointslices.discovery.k8s.io", "-n", namespace, + "-l", "kubernetes.io/service-name=mapserver-operator-webhook-service", + "-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}") + output, err := utils.Run(cmd) + g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist") + g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready") + } + Eventually(verifyWebhookEndpointsReady, 3*time.Minute, time.Second).Should(Succeed()) + + // +kubebuilder:scaffold:e2e-metrics-webhooks-readiness By("creating the curl-metrics pod to access the metrics endpoint") cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", @@ -221,6 +238,7 @@ var _ = Describe("Manager", Ordered, func() { "command": ["/bin/sh", "-c"], "args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"], "securityContext": { + "readOnlyRootFilesystem": true, "allowPrivilegeEscalation": false, "capabilities": { "drop": ["ALL"] @@ -232,7 +250,7 @@ var _ = Describe("Manager", Ordered, func() { } } }], - "serviceAccount": "%s" + "serviceAccountName": "%s" } }`, token, metricsServiceName, namespace, serviceAccountName)) _, err = utils.Run(cmd) @@ -250,10 +268,13 @@ var _ = Describe("Manager", Ordered, func() { Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) By("getting the metrics by checking curl-metrics logs") - metricsOutput := getMetricsOutput() - Expect(metricsOutput).To(ContainSubstring( - "controller_runtime_reconcile_total", - )) + verifyMetricsAvailable := func(g Gomega) { + metricsOutput, err := getMetricsOutput() + g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") + g.Expect(metricsOutput).NotTo(BeEmpty()) + g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) + } + Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed()) }) It("should provisioned cert-manager", func() { @@ -313,7 +334,8 @@ var _ = Describe("Manager", Ordered, func() { // TODO: Customize the e2e test suite with scenarios specific to your project. // Consider applying sample/CR(s) and check their status and/or verifying // the reconciliation by using the metrics, i.e.: - // metricsOutput := getMetricsOutput() + // metricsOutput, err := getMetricsOutput() + // Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") // Expect(metricsOutput).To(ContainSubstring( // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, // strings.ToLower(), @@ -363,13 +385,10 @@ func serviceAccountToken() (string, error) { } // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. -func getMetricsOutput() string { +func getMetricsOutput() (string, error) { By("getting the curl-metrics logs") cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) - metricsOutput, err := utils.Run(cmd) - Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") - Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) - return metricsOutput + return utils.Run(cmd) } // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, diff --git a/test/utils/utils.go b/test/utils/utils.go index 04a5141..b3b8d16 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -24,16 +24,15 @@ import ( "os/exec" "strings" - . "github.com/onsi/ginkgo/v2" //nolint:golint,revive + . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck ) const ( - prometheusOperatorVersion = "v0.77.1" - prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + - "releases/download/%s/bundle.yaml" - - certmanagerVersion = "v1.16.3" + certmanagerVersion = "v1.19.1" certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" + + defaultKindBinary = "kind" + defaultKindCluster = "kind" ) func warnError(err error) { @@ -46,71 +45,40 @@ func Run(cmd *exec.Cmd) (string, error) { cmd.Dir = dir if err := os.Chdir(cmd.Dir); err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) + _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err) } cmd.Env = append(os.Environ(), "GO111MODULE=on") command := strings.Join(cmd.Args, " ") - _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) + _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command) output, err := cmd.CombinedOutput() if err != nil { - return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) + return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err) } return string(output), nil } -// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. -func InstallPrometheusOperator() error { - url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) - cmd := exec.Command("kubectl", "create", "-f", url) - _, err := Run(cmd) - return err -} - -// UninstallPrometheusOperator uninstalls the prometheus -func UninstallPrometheusOperator() { - url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) +// UninstallCertManager uninstalls the cert manager +func UninstallCertManager() { + url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } -} - -// IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed -// by verifying the existence of key CRDs related to Prometheus. -func IsPrometheusCRDsInstalled() bool { - // List of common Prometheus CRDs - prometheusCRDs := []string{ - "prometheuses.monitoring.coreos.com", - "prometheusrules.monitoring.coreos.com", - "prometheusagents.monitoring.coreos.com", - } - cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name") - output, err := Run(cmd) - if err != nil { - return false + // Delete leftover leases in kube-system (not cleaned by default) + kubeSystemLeases := []string{ + "cert-manager-cainjector-leader-election", + "cert-manager-controller", } - crdList := GetNonEmptyLines(output) - for _, crd := range prometheusCRDs { - for _, line := range crdList { - if strings.Contains(line, crd) { - return true - } + for _, lease := range kubeSystemLeases { + cmd = exec.Command("kubectl", "delete", "lease", lease, + "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0") + if _, err := Run(cmd); err != nil { + warnError(err) } } - - return false -} - -// UninstallCertManager uninstalls the cert manager -func UninstallCertManager() { - url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) - cmd := exec.Command("kubectl", "delete", "-f", url) - if _, err := Run(cmd); err != nil { - warnError(err) - } } // InstallCertManager installs the cert manager bundle. @@ -167,12 +135,16 @@ func IsCertManagerCRDsInstalled() bool { // LoadImageToKindClusterWithName loads a local docker image to the kind cluster func LoadImageToKindClusterWithName(name string) error { - cluster := "kind" + cluster := defaultKindCluster if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { cluster = v } kindOptions := []string{"load", "docker-image", name, "--name", cluster} - cmd := exec.Command("kind", kindOptions...) + kindBinary := defaultKindBinary + if v, ok := os.LookupEnv("KIND"); ok { + kindBinary = v + } + cmd := exec.Command(kindBinary, kindOptions...) _, err := Run(cmd) return err } @@ -195,9 +167,9 @@ func GetNonEmptyLines(output string) []string { func GetProjectDir() (string, error) { wd, err := os.Getwd() if err != nil { - return wd, err + return wd, fmt.Errorf("failed to get current working directory: %w", err) } - wd = strings.Replace(wd, "/test/e2e", "", -1) + wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } @@ -208,19 +180,19 @@ func UncommentCode(filename, target, prefix string) error { // nolint:gosec content, err := os.ReadFile(filename) if err != nil { - return err + return fmt.Errorf("failed to read file %q: %w", filename, err) } strContent := string(content) idx := strings.Index(strContent, target) if idx < 0 { - return fmt.Errorf("unable to find the code %s to be uncomment", target) + return fmt.Errorf("unable to find the code %q to be uncomment", target) } out := new(bytes.Buffer) _, err = out.Write(content[:idx]) if err != nil { - return err + return fmt.Errorf("failed to write to output: %w", err) } scanner := bufio.NewScanner(bytes.NewBufferString(target)) @@ -228,24 +200,27 @@ func UncommentCode(filename, target, prefix string) error { return nil } for { - _, err := out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)) - if err != nil { - return err + if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil { + return fmt.Errorf("failed to write to output: %w", err) } // Avoid writing a newline in case the previous line was the last in target. if !scanner.Scan() { break } - if _, err := out.WriteString("\n"); err != nil { - return err + if _, err = out.WriteString("\n"); err != nil { + return fmt.Errorf("failed to write to output: %w", err) } } - _, err = out.Write(content[idx+len(target):]) - if err != nil { - return err + if _, err = out.Write(content[idx+len(target):]); err != nil { + return fmt.Errorf("failed to write to output: %w", err) } + // false positive // nolint:gosec - return os.WriteFile(filename, out.Bytes(), 0644) + if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil { + return fmt.Errorf("failed to write file %q: %w", filename, err) + } + + return nil } From 8101cd5261ff98eec30ae52a82c2653ed7e9bc32 Mon Sep 17 00:00:00 2001 From: Jelle Dijkstra Date: Mon, 22 Dec 2025 16:49:09 +0100 Subject: [PATCH 4/7] Remove plural crds --- config/crd/bases/pdok.nl_wfses.yaml | 236 ---------------------------- config/crd/bases/pdok.nl_wmses.yaml | 236 ---------------------------- 2 files changed, 472 deletions(-) delete mode 100644 config/crd/bases/pdok.nl_wfses.yaml delete mode 100644 config/crd/bases/pdok.nl_wmses.yaml diff --git a/config/crd/bases/pdok.nl_wfses.yaml b/config/crd/bases/pdok.nl_wfses.yaml deleted file mode 100644 index 479bc2b..0000000 --- a/config/crd/bases/pdok.nl_wfses.yaml +++ /dev/null @@ -1,236 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: wfses.pdok.nl -spec: - group: pdok.nl - names: - kind: WFS - listKind: WFSList - plural: wfses - singular: wfs - scope: Namespaced - versions: - - name: v2beta1 - schema: - openAPIV3Schema: - description: WFS is the Schema for the wfs API - 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 desired state of WFS - properties: - foo: - description: foo is an example field of WFS. Edit wfs_types.go to - remove/update - type: string - type: object - status: - description: status defines the observed state of WFS - properties: - conditions: - description: |- - conditions represent the current state of the WFS resource. - Each condition has a unique type and reflects the status of a specific aspect of the resource. - - Standard condition types include: - - "Available": the resource is fully functional - - "Progressing": the resource is being created or updated - - "Degraded": the resource failed to reach or maintain its desired state - - The status of each condition is one of True, False, or Unknown. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - required: - - spec - type: object - served: true - storage: false - subresources: - status: {} - - name: v3 - schema: - openAPIV3Schema: - description: WFS is the Schema for the wfs API - 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 desired state of WFS - properties: - foo: - description: foo is an example field of WFS. Edit wfs_types.go to - remove/update - type: string - type: object - status: - description: status defines the observed state of WFS - properties: - conditions: - description: |- - conditions represent the current state of the WFS resource. - Each condition has a unique type and reflects the status of a specific aspect of the resource. - - Standard condition types include: - - "Available": the resource is fully functional - - "Progressing": the resource is being created or updated - - "Degraded": the resource failed to reach or maintain its desired state - - The status of each condition is one of True, False, or Unknown. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/pdok.nl_wmses.yaml b/config/crd/bases/pdok.nl_wmses.yaml deleted file mode 100644 index 98a0d2c..0000000 --- a/config/crd/bases/pdok.nl_wmses.yaml +++ /dev/null @@ -1,236 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.19.0 - name: wmses.pdok.nl -spec: - group: pdok.nl - names: - kind: WMS - listKind: WMSList - plural: wmses - singular: wms - scope: Namespaced - versions: - - name: v2beta1 - schema: - openAPIV3Schema: - description: WMS is the Schema for the wms API - 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 desired state of WMS - properties: - foo: - description: foo is an example field of WMS. Edit wms_types.go to - remove/update - type: string - type: object - status: - description: status defines the observed state of WMS - properties: - conditions: - description: |- - conditions represent the current state of the WMS resource. - Each condition has a unique type and reflects the status of a specific aspect of the resource. - - Standard condition types include: - - "Available": the resource is fully functional - - "Progressing": the resource is being created or updated - - "Degraded": the resource failed to reach or maintain its desired state - - The status of each condition is one of True, False, or Unknown. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - required: - - spec - type: object - served: true - storage: false - subresources: - status: {} - - name: v3 - schema: - openAPIV3Schema: - description: WMS is the Schema for the wms API - 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 desired state of WMS - properties: - foo: - description: foo is an example field of WMS. Edit wms_types.go to - remove/update - type: string - type: object - status: - description: status defines the observed state of WMS - properties: - conditions: - description: |- - conditions represent the current state of the WMS resource. - Each condition has a unique type and reflects the status of a specific aspect of the resource. - - Standard condition types include: - - "Available": the resource is fully functional - - "Progressing": the resource is being created or updated - - "Degraded": the resource failed to reach or maintain its desired state - - The status of each condition is one of True, False, or Unknown. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} From 4931206f380107dee25a050d5c169f6d415a0219 Mon Sep 17 00:00:00 2001 From: Jelle Dijkstra Date: Mon, 22 Dec 2025 16:52:36 +0100 Subject: [PATCH 5/7] Restore dockerignore --- .dockerignore | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.dockerignore b/.dockerignore index 9af8280..0afd217 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,3 @@ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file -# Ignore everything by default and re-include only needed files -** - -# Re-include Go source files (but not *_test.go) -!**/*.go -**/*_test.go - -# Re-include Go module files -!go.mod -!go.sum +# Ignore build and test binaries. +bin/ \ No newline at end of file From db075c038f4bfc12c77ee3a0f185b0d170a4f158 Mon Sep 17 00:00:00 2001 From: Jelle Dijkstra Date: Mon, 22 Dec 2025 17:02:52 +0100 Subject: [PATCH 6/7] Go 1.25 --- go.mod | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8caf909..68de7cb 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/pdok/mapserver-operator -go 1.24.0 - -toolchain go1.24.2 +go 1.25 godebug default=go1.23 From 4ca0adb37a3724de950f6d202ec9cfd1751c5e5a Mon Sep 17 00:00:00 2001 From: Jelle Dijkstra Date: Mon, 22 Dec 2025 17:03:10 +0100 Subject: [PATCH 7/7] Go 1.25 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 68de7cb..25985e7 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/pdok/mapserver-operator go 1.25 -godebug default=go1.23 +godebug default=go1.25 require ( github.com/cbroglie/mustache v1.4.0