Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v3/atom_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@ var baseURL string
var blobEndpoint string

// AtomSpec defines the desired state of Atom.
// +kubebuilder:validation:XValidation:rule="!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x, x.url == self.service.baseUrl)",messageExpression="'ingressRouteUrls should include service.baseUrl '+self.service.baseUrl"
type AtomSpec struct {
// Optional lifecycle settings
Lifecycle *smoothoperatormodel.Lifecycle `json:"lifecycle,omitempty"`

// Optional list of URLs where the service can be reached
// By default only the spec.service.baseUrl is used
IngressRouteURLs smoothoperatormodel.IngressRouteURLs `json:"ingressRouteUrls,omitempty"`

// Service specification
Service Service `json:"service"`
}
Expand Down
14 changes: 13 additions & 1 deletion api/v3/atom_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func (atom *Atom) ValidateUpdate(c client.Client, atomOld *Atom) ([]string, erro
field.NewPath("spec").Child("service").Child("baseUrl"),
)

smoothoperatorvalidation.ValidateIngressRouteURLsNotRemoved(atomOld.Spec.IngressRouteURLs, atom.Spec.IngressRouteURLs, &allErrs, nil)

ValidateAtom(c, atom, &warnings, &allErrs)

if len(allErrs) == 0 {
Expand Down Expand Up @@ -87,9 +89,19 @@ func ValidateAtomWithoutClusterChecks(atom *Atom, warnings *[]string, allErrs *f
fieldPath = field.NewPath("metadata").Child("name")
smoothoperatorvalidation.AddWarning(warnings, *fieldPath, "should not contain atom", atom.GroupVersionKind(), atom.GetName())
}

validateDatasetFeeds(atom, allErrs)

err := smoothoperatorvalidation.ValidateIngressRouteURLsContainsBaseURL(atom.Spec.IngressRouteURLs, atom.Spec.Service.BaseURL, nil)
if err != nil {
*allErrs = append(*allErrs, err)
}
}

func validateDatasetFeeds(atom *Atom, allErrs *field.ErrorList) {
var feedNames []string
for i, datasetFeed := range atom.Spec.Service.DatasetFeeds {
fieldPath = field.NewPath("spec").Child("service").Child("datasetFeeds").Index(i)
fieldPath := field.NewPath("spec").Child("service").Child("datasetFeeds").Index(i)

if slices.Contains(feedNames, datasetFeed.TechnicalName) {
*allErrs = append(*allErrs, field.Duplicate(fieldPath.Child("technicalName"), datasetFeed.TechnicalName))
Expand Down
7 changes: 7 additions & 0 deletions api/v3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions config/crd/bases/pdok.nl_atoms.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,21 @@ spec:
spec:
description: AtomSpec defines the desired state of Atom.
properties:
ingressRouteUrls:
description: |-
Optional list of URLs where the service can be reached
By default only the spec.service.baseUrl 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:
Expand Down Expand Up @@ -704,6 +719,11 @@ spec:
required:
- service
type: object
x-kubernetes-validations:
- messageExpression: '''ingressRouteUrls should include service.baseUrl
''+self.service.baseUrl'
rule: '!has(self.ingressRouteUrls) || self.ingressRouteUrls.exists_one(x,
x.url == self.service.baseUrl)'
status:
description: OperatorStatus defines the observed state of an Atom/WFS/WMS/....
properties:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/onsi/ginkgo/v2 v2.22.1
github.com/onsi/gomega v1.36.2
github.com/pdok/atom-generator v0.6.4
github.com/pdok/smooth-operator v0.1.1
github.com/pdok/smooth-operator v0.1.2
github.com/peterbourgon/ff v1.7.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.10.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pdok/atom-generator v0.6.4 h1:UpTTSKskdWLnxTAhSNOlz8dF2tBousD97V03kWzCV2k=
github.com/pdok/atom-generator v0.6.4/go.mod h1:IlPwti5ocXDTjB4xmz0ZpHCOW/suuW5gQMfjfwPX6uM=
github.com/pdok/smooth-operator v0.1.1 h1:rmsup4HmzJsxt4ZT9GWfj498dKLRfDhyuILeEkjju/A=
github.com/pdok/smooth-operator v0.1.1/go.mod h1:przwM7mBGmNPqabyhImKVZ15WL4zbqLqH4ExbuWKhWE=
github.com/pdok/smooth-operator v0.1.2 h1:ibGRgFjKysu665IUToQhs34hHhptftI+7RBFrGNw4nQ=
github.com/pdok/smooth-operator v0.1.2/go.mod h1:przwM7mBGmNPqabyhImKVZ15WL4zbqLqH4ExbuWKhWE=
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=
Expand Down
109 changes: 49 additions & 60 deletions internal/controller/ingressroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"sort"
"strconv"

smoothoperatormodel "github.com/pdok/smooth-operator/model"

pdoknlv3 "github.com/pdok/atom-operator/api/v3"
smoothutil "github.com/pdok/smooth-operator/pkg/util"
traefikiov1alpha1 "github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
Expand Down Expand Up @@ -40,61 +42,8 @@ func (r *AtomReconciler) mutateIngressRoute(atom *pdoknlv3.Atom, ingressRoute *t
"uptime.pdok.nl/tags": "public-stats,atom",
}

ingressRoute.Spec = traefikiov1alpha1.IngressRouteSpec{
Routes: []traefikiov1alpha1.Route{
{
Kind: "Rule",
Match: getMatchRule(baseURL.JoinPath("index.xml"), false),
Services: []traefikiov1alpha1.Service{
{
LoadBalancerSpec: traefikiov1alpha1.LoadBalancerSpec{
Name: getBareService(atom).GetName(),
Kind: "Service",
Port: intstr.FromInt32(atomPortNr),
},
},
},
Middlewares: []traefikiov1alpha1.MiddlewareRef{
{
Name: atom.Name + headersSuffix,
},
{
Name: atom.Name + stripPrefixSuffix,
},
},
},
},
}

// Set additional routes per datasetFeed
for _, datasetFeed := range atom.Spec.Service.DatasetFeeds {
matchRule := getMatchRule(baseURL.JoinPath(datasetFeed.TechnicalName+".xml"), false)
rule := getDefaultRule(atom, matchRule)
ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, rule)
}

azureStorageRule := traefikiov1alpha1.Route{
Kind: "Rule",
Match: getMatchRule(baseURL.JoinPath("downloads/"), true),
Services: []traefikiov1alpha1.Service{
{
LoadBalancerSpec: traefikiov1alpha1.LoadBalancerSpec{
Name: "azure-storage",
Port: intstr.IntOrString{Type: intstr.String, StrVal: "azure-storage"},
PassHostHeader: smoothutil.Pointer(false),
Kind: "Service",
},
},
},
Middlewares: []traefikiov1alpha1.MiddlewareRef{
{
Name: atom.Name + headersSuffix,
},
},
}

var downloadMiddlewares []traefikiov1alpha1.MiddlewareRef
// Set additional Azure storage middleware per download link
var downloadMiddlewares []traefikiov1alpha1.MiddlewareRef
for _, group := range getDownloadLinkGroups(atom.GetDownloadLinks()) {
middlewareRef := traefikiov1alpha1.MiddlewareRef{
Name: atom.Name + downloadsSuffix + strconv.Itoa(*group.index),
Expand All @@ -106,12 +55,14 @@ func (r *AtomReconciler) mutateIngressRoute(atom *pdoknlv3.Atom, ingressRoute *t
return downloadMiddlewares[i].Name < downloadMiddlewares[j].Name
})

azureStorageRule.Middlewares = append(azureStorageRule.Middlewares, downloadMiddlewares...)

ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, azureStorageRule)

// Add finalizers
ingressRoute.Finalizers = []string{"uptime.pdok.nl/finalizer"}
ingressRoute.Spec.Routes = []traefikiov1alpha1.Route{}
if len(atom.Spec.IngressRouteURLs) > 0 {
for _, ingressRouteURL := range atom.Spec.IngressRouteURLs {
ingressRoute.Spec.Routes = append(ingressRoute.Spec.Routes, getRoutesForURL(atom, ingressRouteURL.URL, downloadMiddlewares)...)
}
} else {
ingressRoute.Spec.Routes = getRoutesForURL(atom, atom.Spec.Service.BaseURL, downloadMiddlewares)
}

if err := smoothutil.EnsureSetGVK(r.Client, ingressRoute, ingressRoute); err != nil {
return err
Expand Down Expand Up @@ -152,3 +103,41 @@ func getDefaultRule(atom *pdoknlv3.Atom, matchRule string) traefikiov1alpha1.Rou
},
}
}

func getRoutesForURL(atom *pdoknlv3.Atom, url smoothoperatormodel.URL, downloadMiddlewares []traefikiov1alpha1.MiddlewareRef) []traefikiov1alpha1.Route {
routes := []traefikiov1alpha1.Route{
getDefaultRule(atom, getMatchRule(url.JoinPath("index.xml"), false)),
}

// Set additional routes per datasetFeed
for _, datasetFeed := range atom.Spec.Service.DatasetFeeds {
matchRule := getMatchRule(url.JoinPath(datasetFeed.TechnicalName+".xml"), false)
rule := getDefaultRule(atom, matchRule)
routes = append(routes, rule)
}

// Add Azure storage rule
azureStorageRule := traefikiov1alpha1.Route{
Kind: "Rule",
Match: getMatchRule(url.JoinPath("downloads/"), true),
Services: []traefikiov1alpha1.Service{
{
LoadBalancerSpec: traefikiov1alpha1.LoadBalancerSpec{
Name: "azure-storage",
Port: intstr.IntOrString{Type: intstr.String, StrVal: "azure-storage"},
PassHostHeader: smoothutil.Pointer(false),
Kind: "Service",
},
},
},
Middlewares: append([]traefikiov1alpha1.MiddlewareRef{
{
Name: atom.Name + headersSuffix,
}},
downloadMiddlewares...,
),
}
routes = append(routes, azureStorageRule)

return routes
}
21 changes: 17 additions & 4 deletions internal/controller/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,14 @@ func (r *AtomReconciler) mutateDownloadLinkMiddleware(atom *pdoknlv3.Atom, prefi
return err
}

baseURL := atom.Spec.Service.BaseURL
ingressRouteURLs := atom.Spec.IngressRouteURLs
if len(ingressRouteURLs) == 0 {
ingressRouteURLs = smoothoperatormodel.IngressRouteURLs{{URL: atom.Spec.Service.BaseURL}}
}

middleware.Spec = traefikiov1alpha1.MiddlewareSpec{
ReplacePathRegex: &dynamic.ReplacePathRegex{
Regex: getDownloadLinkRegex(baseURL, files),
Regex: getDownloadLinkRegex(ingressRouteURLs, files),
Replacement: "/" + prefix + "/$1",
},
}
Expand All @@ -108,8 +111,18 @@ func (r *AtomReconciler) mutateDownloadLinkMiddleware(atom *pdoknlv3.Atom, prefi
return ctrl.SetControllerReference(atom, middleware, r.Scheme)
}

func getDownloadLinkRegex(url smoothoperatormodel.URL, files []string) string {
return "^" + url.JoinPath("downloads", "("+strings.Join(files, "|")+")").Path
func getDownloadLinkRegex(ingressRouteURLs smoothoperatormodel.IngressRouteURLs, files []string) string {
if len(ingressRouteURLs) == 1 {
return "^" + ingressRouteURLs[0].URL.JoinPath("downloads", "("+strings.Join(files, "|")+")").Path
}

paths := []string{}
for _, ingressRouteURL := range ingressRouteURLs {
paths = append(paths, ingressRouteURL.URL.Path)
}

return "^(" + strings.Join(paths, "|") + ")/downloads/" + "(" + strings.Join(files, "|") + ")"

}

func getDownloadLinkGroups(links []pdoknlv3.DownloadLink) map[string]struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ metadata:
uid: ""
blockOwnerDeletion: true
controller: true
finalizers:
- uptime.pdok.nl/finalizer
spec:
routes:
- kind: Rule
Expand Down Expand Up @@ -61,3 +59,42 @@ spec:
- name: maximum-atom-downloads-0
- name: maximum-atom-downloads-1
- name: maximum-atom-downloads-2
- kind: Rule
match: (Host(`localhost`) || Host(`test.com`)) && Path(`/path/other/index.xml`)
services:
- kind: Service
name: maximum-atom
port: 80
middlewares:
- name: maximum-atom-headers
- name: maximum-atom-prefixstrip
- kind: Rule
match: (Host(`localhost`) || Host(`test.com`)) && Path(`/path/other/feed-1.xml`)
services:
- kind: Service
name: maximum-atom
port: 80
middlewares:
- name: maximum-atom-headers
- name: maximum-atom-prefixstrip
- kind: Rule
match: (Host(`localhost`) || Host(`test.com`)) && Path(`/path/other/feed-2.xml`)
services:
- kind: Service
name: maximum-atom
port: 80
middlewares:
- name: maximum-atom-headers
- name: maximum-atom-prefixstrip
- kind: Rule
match: (Host(`localhost`) || Host(`test.com`)) && PathPrefix(`/path/other/downloads/`)
services:
- kind: Service
name: azure-storage
port: azure-storage
passHostHeader: false
middlewares:
- name: maximum-atom-headers
- name: maximum-atom-downloads-0
- name: maximum-atom-downloads-1
- name: maximum-atom-downloads-2
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ metadata:
controller: true
spec:
replacePathRegex:
regex: ^/path/downloads/(index.json|file-1.ext)
regex: ^(/path|/path/other)/downloads/(index.json|file-1.ext)
replacement: /container/prefix-1/$1
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ metadata:
controller: true
spec:
replacePathRegex:
regex: ^/path/downloads/(file-2.ext)
regex: ^(/path|/path/other)/downloads/(file-2.ext)
replacement: /container/prefix-2/$1
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ metadata:
controller: true
spec:
replacePathRegex:
regex: ^/path/downloads/(file-3.ext|file-4.ext)
regex: ^(/path|/path/other)/downloads/(file-3.ext|file-4.ext)
replacement: /container/prefix-3/$1
3 changes: 3 additions & 0 deletions internal/controller/test_data/maximum-atom/input/atom.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ metadata:
labels:
test: test
spec:
ingressRouteUrls:
- url: https://test.com/path/
- url: https://test.com/path/other/
service:
baseUrl: https://test.com/path/
stylesheet: https://test.com/stylesheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ metadata:
uid: ""
blockOwnerDeletion: true
controller: true
finalizers:
- uptime.pdok.nl/finalizer
spec:
routes:
- kind: Rule
Expand Down
Loading