diff --git a/api/v3/shared_types.go b/api/v3/shared_types.go index 48427b3..9499375 100644 --- a/api/v3/shared_types.go +++ b/api/v3/shared_types.go @@ -22,6 +22,7 @@ const ( ServiceTypeWFS ServiceType = "WFS" ) +// WMSWFS is the common interface used for both WMS and WFS resources. // +kubebuilder:object:generate=false type WMSWFS interface { *WFS | *WMS @@ -35,86 +36,177 @@ type WMSWFS interface { HasPostgisData() bool // Sha1 hash of the objects name ID() string + // URLPath returns the configured service URL URLPath() string GeoPackages() []*Gpkg } +// 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"` } +// Options configures optional behaviors of the operator, like ingress, casing, and data prefetching. +// +kubebuilder:validation:Type=object type Options struct { + // IncludeIngress dictates whether to deploy an Ingress or ensure none exists. // +kubebuilder:default:=true IncludeIngress bool `json:"includeIngress"` + // AutomaticCasing enables automatic conversion from snake_case to camelCase. // +kubebuilder:default:=true AutomaticCasing bool `json:"automaticCasing"` + // ValidateRequests enables request validation against the service schema. // +kubebuilder:default:=true ValidateRequests *bool `json:"validateRequests,omitempty"` + // RewriteGroupToDataLayers merges group layers into individual data layers. // +kubebuilder:default:=false RewriteGroupToDataLayers *bool `json:"rewriteGroupToDataLayers,omitempty"` + // DisableWebserviceProxy disables the built-in proxy for external web services. // +kubebuilder:default:=false DisableWebserviceProxy *bool `json:"disableWebserviceProxy,omitempty"` + // 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 PrefetchData *bool `json:"prefetchData,omitempty"` + // ValidateChildStyleNameEqual ensures child style names match the parent style. + // +kubebuilder:default=false ValidateChildStyleNameEqual *bool `json:"validateChildStyleNameEqual,omitempty"` } +// Inspire holds INSPIRE-specific metadata for the service. +// +kubebuilder:validation:Type=object type Inspire struct { - ServiceMetadataURL MetadataURL `json:"serviceMetadataUrl"` - SpatialDatasetIdentifier string `json:"spatialDatasetIdentifier"` - Language string `json:"language"` + // ServiceMetadataURL references the CSW or custom metadata record. + // +kubebuilder:validation:Type=object + ServiceMetadataURL MetadataURL `json:"serviceMetadataUrl"` + + // SpatialDatasetIdentifier is the ID uniquely identifying the dataset. + // +kubebuilder:validation:MinLength:=1 + SpatialDatasetIdentifier string `json:"spatialDatasetIdentifier"` + + // Language of the INSPIRE metadata record + // +kubebuilder:validation:MinLength:=1 + Language string `json:"language"` } type MetadataURL struct { - CSW *Metadata `json:"csw"` - Custom *Custom `json:"custom,omitempty"` + // CSW describes a metadata record via a metadataIdentifier (UUID). + CSW *Metadata `json:"csw"` + + // 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:Format:=uuid + // +kubebuilder:validation:MinLength:=1 MetadataIdentifier string `json:"metadataIdentifier"` } +// Custom represents a non-CSW metadata link with a href and MIME type. +// +kubebuilder:validation:Type=object type Custom struct { + // +kubebuilder:validation:Pattern=`^https?://.*$` + // +kubebuilder:validation:MinLength=1 Href string `json:"href"` + + // MIME type of the custom link + // +kubebuilder:validation:MinLength=1 Type string `json:"type"` } +// 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 { - Gpkg *Gpkg `json:"gpkg,omitempty"` + // Gpkg configures a GeoPackage file source + Gpkg *Gpkg `json:"gpkg,omitempty"` + + // Postgis configures a Postgis table source Postgis *Postgis `json:"postgis,omitempty"` - TIF *TIF `json:"tif,omitempty"` + + // 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 string `json:"blobKey"` - TableName string `json:"tableName"` - GeometryType string `json:"geometryType"` - Columns []Column `json:"columns"` + // Blobkey identifies the location/bucket of the .gpkg file + // +kubebuilder:validation:Pattern=`\.gpkg$` + // +kubebuilder:validation:MinLength:=1 + 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)$` + // +kubebuilder:validation:MinLength:=1 + 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 string `json:"tableName"` - GeometryType string `json:"geometryType"` - Columns []Column `json:"columns"` + // TableName in postGIS + // +kubebuilder:validation:MinLength=1 + TableName string `json:"tableName"` + + // GeometryType of the table + // +kubebuilder:validation:Pattern=`^(Multi)?(Point|LineString|Polygon)$` + // +kubebuilder:validation:MinLength:=1 + 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 string `json:"blobKey"` - Resample *string `json:"resample,omitempty"` - Offsite *string `json:"offsite,omitempty"` - GetFeatureInfoIncludesClass *bool `json:"getFeatureInfoIncludesClass,omitempty"` + // BlobKey to the TIFF file + // +kubebuilder:validation:Pattern=`\.(tif|tiff)$` + // +kubebuilder:validation:MinLength:=1 + BlobKey string `json:"blobKey"` + + // Resample method + // +kubebuilder:validation:MinLength:=1 + Resample *string `json:"resample,omitempty"` + + // Offsite color for nodata removal + // +kubebuilder:validation:MinLength:=1 + Offsite *string `json:"offsite,omitempty"` + + // Include class names in GetFeatureInfo responses + 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 string `json:"name"` + // 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"` } diff --git a/api/v3/wfs_types.go b/api/v3/wfs_types.go index 94d5c95..2278166 100644 --- a/api/v3/wfs_types.go +++ b/api/v3/wfs_types.go @@ -66,7 +66,9 @@ func init() { // WFSSpec vertegenwoordigt de hoofdstruct voor de YAML-configuratie type WFSSpec struct { + // Optional lifecycle settings Lifecycle *shared_model.Lifecycle `json:"lifecycle,omitempty"` + // +kubebuilder:validation:Type=object // +kubebuilder:validation:Schemaless // +kubebuilder:pruning:PreserveUnknownFields @@ -74,51 +76,124 @@ type WFSSpec struct { PodSpecPatch *corev1.PodSpec `json:"podSpecPatch,omitempty"` HorizontalPodAutoscalerPatch *autoscalingv2.HorizontalPodAutoscalerSpec `json:"horizontalPodAutoscalerPatch,omitempty"` Options Options `json:"options,omitempty"` - Service WFSService `json:"service"` + + // service configuration + Service WFSService `json:"service"` } type WFSService struct { // Geonovum subdomein // +kubebuilder:validation:MinLength:=1 - Prefix string `json:"prefix"` - URL string `json:"url"` - Inspire *Inspire `json:"inspire,omitempty"` - Mapfile *Mapfile `json:"mapfile,omitempty"` - OwnerInfoRef string `json:"ownerInfoRef"` - Title string `json:"title"` - Abstract string `json:"abstract"` - Keywords []string `json:"keywords"` - Fees *string `json:"fees,omitempty"` + Prefix string `json:"prefix"` + + // URL of the service + // +kubebuilder:validation:Pattern:=`^https?://.*$` + // +kubebuilder:validation:MinLength:=1 + URL string `json:"url"` + + // Config for Inspire services + Inspire *Inspire `json:"inspire,omitempty"` + + // 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 + Keywords []string `json:"keywords"` + + // Optional Fees + // +kubebuilder:validation:MinLength:=1 + Fees *string `json:"fees,omitempty"` + + // AccessConstraints URL + // +kubebuilder:validation:Pattern:="https?://" // +kubebuilder:default="https://creativecommons.org/publicdomain/zero/1.0/deed.nl" - AccessConstraints string `json:"accessConstraints"` - DefaultCrs string `json:"defaultCrs"` - OtherCrs []string `json:"otherCrs,omitempty"` - Bbox *Bbox `json:"bbox,omitempty"` + // +kubebuilder:validation:MinLength:=1 + AccessConstraints string `json:"accessConstraints"` + + // Default CRS (DataEPSG) + // +kubebuilder:validation:Pattern:="^EPSG:(28992|25831|25832|3034|3035|3857|4258|4326)$" + // +kubebuilder:validation:MinLength:=1 + DefaultCrs string `json:"defaultCrs"` + + // Other supported CRS + // +kubebuilder:validation:MinItems:=1 + OtherCrs []string `json:"otherCrs,omitempty"` + + // Service bounding box + Bbox *Bbox `json:"bbox,omitempty"` + // CountDefault -> wfs_maxfeatures in mapfile - CountDefault *string `json:"countDefault,omitempty"` + // +kubebuilder:validation:MinLength:=1 + CountDefault *string `json:"countDefault,omitempty"` + + // FeatureTypes configurations + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:Type=array FeatureTypes []FeatureType `json:"featureTypes"` } type Bbox struct { // EXTENT/wfs_extent in mapfile //nolint:tagliatelle + // +kubebuilder:validation:Type=object DefaultCRS shared_model.BBox `json:"defaultCRS"` } +// FeatureType defines a WFS feature type FeatureType struct { - Name string `json:"name"` - Title string `json:"title"` - Abstract string `json:"abstract"` - Keywords []string `json:"keywords"` - DatasetMetadataURL MetadataURL `json:"datasetMetadataUrl"` - Bbox *FeatureBbox `json:"bbox,omitempty"` - Data Data `json:"data"` + // Name of the feature + // +kubebuilder:validation:MinLength:=1 + 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 + Keywords []string `json:"keywords"` + + // Metadata URL + // +kubebuilder:validation:Type=object + DatasetMetadataURL MetadataURL `json:"datasetMetadataUrl"` + + // Optional feature bbox + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type:=object + Bbox *FeatureBbox `json:"bbox,omitempty"` + + // FeatureType data connection + // +kubebuilder:validation:Type=object + Data Data `json:"data"` } +// FeatureType bounding box, if provided it overrides the default extent type FeatureBbox struct { + // DefaultCRS defines the feature’s bounding box in the service’s own CRS //nolint:tagliatelle - DefaultCRS shared_model.BBox `json:"defaultCRS"` - WGS84 *shared_model.BBox `json:"wgs84,omitempty"` + // +kubebuilder:validation:Type=object + DefaultCRS shared_model.BBox `json:"defaultCRS"` + + // WGS84, if provided, gives the same bounding box reprojected into EPSG:4326. + // +kubebuilder:validation:Type=object + WGS84 *shared_model.BBox `json:"wgs84,omitempty"` } func (wfs *WFS) HasPostgisData() bool { diff --git a/config/crd/bases/pdok.nl_wfs.yaml b/config/crd/bases/pdok.nl_wfs.yaml index 6316313..cfcc301 100644 --- a/config/crd/bases/pdok.nl_wfs.yaml +++ b/config/crd/bases/pdok.nl_wfs.yaml @@ -1086,32 +1086,51 @@ spec: - scaleTargetRef type: object 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 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 required: - automaticCasing @@ -1123,13 +1142,20 @@ spec: 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 + minLength: 1 + pattern: https?:// type: string bbox: + description: Service bounding box properties: defaultCRS: description: EXTENT/wfs_extent in mapfile @@ -1161,18 +1187,28 @@ spec: type: object countDefault: description: CountDefault -> wfs_maxfeatures in mapfile + minLength: 1 type: string defaultCrs: + description: Default CRS (DataEPSG) + minLength: 1 + 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: BBox defines a bounding box with coordinates + description: DefaultCRS defines the feature’s bounding + box in the service’s own CRS properties: maxx: description: Rechtsonder X coördinaat @@ -1197,7 +1233,8 @@ spec: - miny type: object wgs84: - description: BBox defines a bounding box with coordinates + description: WGS84, if provided, gives the same bounding + box reprojected into EPSG:4326. properties: maxx: description: Rechtsonder X coördinaat @@ -1225,25 +1262,47 @@ spec: - defaultCRS 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 + minLength: 1 + 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 + minLength: 1 + pattern: ^(Multi)?(Point|LineString|Polygon)$ type: string tableName: + description: TableName is the table within the geopackage + minLength: 1 type: string required: - blobKey @@ -1252,23 +1311,37 @@ spec: - tableName type: object postgis: - description: Postgis - reference to table in a Postgres - database + 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 + minLength: 1 + pattern: ^(Multi)?(Point|LineString|Polygon)$ type: string tableName: + description: TableName in postGIS + minLength: 1 type: string required: - columns @@ -1276,33 +1349,59 @@ spec: - tableName type: object tif: + description: TIF configures a GeoTIF raster source properties: blobKey: + description: BlobKey to the TIFF file + minLength: 1 + pattern: \.(tif|tiff)$ type: string getFeatureInfoIncludesClass: + description: Include class names in GetFeatureInfo + responses type: boolean offsite: + description: Offsite color for nodata removal + minLength: 1 type: string resample: + description: Resample method + minLength: 1 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: Metadata URL properties: csw: + description: CSW describes a metadata record via a metadataIdentifier + (UUID). properties: metadataIdentifier: + description: MetadataIdentifier is the record's + UUID + format: uuid + minLength: 1 type: string required: - metadataIdentifier type: object custom: + description: Custom allows arbitrary href properties: href: + minLength: 1 + pattern: ^https?://.*$ type: string type: + description: MIME type of the custom link + minLength: 1 type: string required: - href @@ -1312,12 +1411,18 @@ spec: - csw type: object keywords: + description: Keywords of the feature items: type: string + minItems: 1 type: array name: + description: Name of the feature + minLength: 1 type: string title: + description: Title of the feature + minLength: 1 type: string required: - abstract @@ -1327,27 +1432,45 @@ spec: - name - title type: object + minItems: 1 type: array fees: + description: Optional Fees + minLength: 1 type: string inspire: + description: Config for Inspire services properties: language: + description: Language of the INSPIRE metadata record + minLength: 1 type: string serviceMetadataUrl: + description: ServiceMetadataURL references the CSW or custom + metadata record. properties: csw: + description: CSW describes a metadata record via a metadataIdentifier + (UUID). properties: metadataIdentifier: + description: MetadataIdentifier is the record's UUID + format: uuid + minLength: 1 type: string required: - metadataIdentifier type: object custom: + description: Custom allows arbitrary href properties: href: + minLength: 1 + pattern: ^https?://.*$ type: string type: + description: MIME type of the custom link + minLength: 1 type: string required: - href @@ -1357,6 +1480,9 @@ spec: - csw type: object spatialDatasetIdentifier: + description: SpatialDatasetIdentifier is the ID uniquely identifying + the dataset. + minLength: 1 type: string required: - language @@ -1364,10 +1490,13 @@ spec: - spatialDatasetIdentifier type: object keywords: + description: Keywords for capabilities items: type: string + minItems: 1 type: array mapfile: + description: External Mapfile reference properties: configMapKeyRef: description: Selects a key from a ConfigMap. @@ -1396,18 +1525,27 @@ spec: - configMapKeyRef type: object otherCrs: + description: Other supported CRS items: 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 + minLength: 1 + pattern: ^https?://.*$ type: string required: - abstract diff --git a/internal/controller/test_manifests/v3_wfs.yaml b/internal/controller/test_manifests/v3_wfs.yaml index 4d508b5..3282e36 100644 --- a/internal/controller/test_manifests/v3_wfs.yaml +++ b/internal/controller/test_manifests/v3_wfs.yaml @@ -44,7 +44,7 @@ spec: serviceMetadataUrl: csw: metadataIdentifier: 68a42961-ed55-436b-a412-cc7424fd2a6e - spatialDatasetIdentifier: "" + spatialDatasetIdentifier: "002993" language: "dut" ownerInfoRef: pdok title: "Dataset" @@ -52,8 +52,7 @@ spec: keywords: - keyword1 - keyword2 - fees: "" - accessConstraints: "" + accessConstraints: "https://test.com" defaultCrs: "EPSG:28992" bbox: defaultCRS: