Skip to content

Commit 1957def

Browse files
committed
feat: remote portal
Signed-off-by: David Sabatie <david.sabatie@notrenet.com>
1 parent 87d014d commit 1957def

31 files changed

Lines changed: 1961 additions & 98 deletions

CLAUDE.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,32 @@ type PortalSpec struct {
185185
Title string `json:"title"` // Display title
186186
Main bool `json:"main,omitempty"` // Default portal flag
187187
SubPath string `json:"subPath,omitempty"` // URL subpath (defaults to name)
188+
URL string `json:"url,omitempty"` // Remote portal URL (cannot be set if Main=true)
188189
}
189190

190191
// Status
191192
type PortalStatus struct {
192193
Ready bool
193194
Conditions []metav1.Condition
195+
RemoteSync *RemoteSyncStatus // Only populated when URL is set
196+
}
197+
198+
// RemoteSyncStatus contains status for remote portal synchronization
199+
type RemoteSyncStatus struct {
200+
LastSyncTime *metav1.Time // Last successful sync timestamp
201+
LastSyncError string // Error from last failed sync (empty if success)
202+
RemoteTitle string // Title fetched from remote portal
203+
FQDNCount int // Number of FQDNs fetched from remote
194204
}
195205
```
196206

207+
**Remote Portal Feature:**
208+
- When `spec.url` is set, the Portal fetches DNS data from a remote SRE Portal instance
209+
- The main portal (`spec.main=true`) cannot have a URL (validated by webhook)
210+
- Remote portals are excluded from local source collection (SourceController)
211+
- PortalReconciler periodically syncs with remote portals (every 5 minutes)
212+
- Remote portal status includes sync time, error state, and FQDN count
213+
197214
## Controller Pattern: Chain of Responsibility
198215

199216
All controllers use a generic Chain-of-Responsibility framework defined in `internal/reconciler/handler.go`:

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ generate-all: manifests generate proto ## Generate all code (CRDs, DeepCopy, pro
138138
run: manifests generate fmt vet generate-certs ## Run a controller from your host.
139139
go run ./cmd/main.go -zap-devel --webhook-cert-path=$(CERTSDIR) --config=./config/samples/test_config.yaml
140140

141+
.PHONY: run2
142+
run2: manifests generate fmt vet generate-certs2 ## Run a controller from your host.
143+
go run ./cmd/main.go -zap-devel --web-bind-address=:8082 --health-probe-bind-address=:9091 --webhook-port=9444 --webhook-cert-path=$(CERTSDIR) --config=./config/samples/test_config.yaml
144+
141145
# If you wish to build the manager image targeting other platforms you can use the --platform flag.
142146
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
143147
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
@@ -304,3 +308,10 @@ generate-certs: ## Generates the certs required to run webhooks locally
304308
cd $(CERTSDIR) && \
305309
openssl genrsa 2048 > tls.key && \
306310
openssl req -new -x509 -nodes -sha256 -days 365 -key tls.key -out tls.crt -subj "/C=XX"
311+
312+
.PHONY: generate-certs2
313+
generate-certs2: ## Generates the certs required to run webhooks locally
314+
mkdir -p $(CERTSDIR)
315+
cd $(CERTSDIR) && \
316+
openssl genrsa 2048 > tls.key2 && \
317+
openssl req -new -x509 -nodes -sha256 -days 365 -key tls.key2 -out tls.crt2 -subj "/C=XX"

api/v1alpha1/dns_types.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ type FQDNGroupStatus struct {
8686
// +optional
8787
Description string `json:"description,omitempty"`
8888

89-
// source indicates where this group came from (manual or external-dns)
90-
// +kubebuilder:validation:Enum=manual;external-dns
89+
// source indicates where this group came from (manual, external-dns, or remote)
90+
// +kubebuilder:validation:Enum=manual;external-dns;remote
9191
Source string `json:"source"`
9292

9393
// fqdns is the list of FQDNs in this group

api/v1alpha1/portal_types.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ type PortalSpec struct {
3434
// subPath is the URL subpath for this portal (defaults to metadata.name)
3535
// +optional
3636
SubPath string `json:"subPath,omitempty"`
37+
38+
// remote configures this portal to fetch data from a remote SRE Portal instance.
39+
// When set, the operator will fetch DNS information from the remote portal
40+
// instead of collecting data from the local cluster.
41+
// This field cannot be set when main is true.
42+
// +optional
43+
Remote *RemotePortalSpec `json:"remote,omitempty"`
44+
}
45+
46+
// RemotePortalSpec defines the configuration for fetching data from a remote portal.
47+
type RemotePortalSpec struct {
48+
// url is the base URL of the remote SRE Portal instance.
49+
// +kubebuilder:validation:Required
50+
// +kubebuilder:validation:Pattern=`^https?://.*`
51+
URL string `json:"url"`
52+
53+
// portal is the name of the portal to target on the remote instance.
54+
// If not set, the main portal of the remote instance will be used.
55+
// +optional
56+
Portal string `json:"portal,omitempty"`
3757
}
3858

3959
// PortalStatus defines the observed state of Portal.
@@ -47,13 +67,39 @@ type PortalStatus struct {
4767
// +listMapKey=type
4868
// +optional
4969
Conditions []metav1.Condition `json:"conditions,omitempty"`
70+
71+
// remoteSync contains the status of synchronization with a remote portal.
72+
// This is only populated when spec.remote is set.
73+
// +optional
74+
RemoteSync *RemoteSyncStatus `json:"remoteSync,omitempty"`
75+
}
76+
77+
// RemoteSyncStatus contains status information about remote portal synchronization.
78+
type RemoteSyncStatus struct {
79+
// lastSyncTime is the timestamp of the last successful synchronization.
80+
// +optional
81+
LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"`
82+
83+
// lastSyncError contains the error message from the last failed synchronization attempt.
84+
// Empty if the last sync was successful.
85+
// +optional
86+
LastSyncError string `json:"lastSyncError,omitempty"`
87+
88+
// remoteTitle is the title of the remote portal as fetched from the remote server.
89+
// +optional
90+
RemoteTitle string `json:"remoteTitle,omitempty"`
91+
92+
// fqdnCount is the number of FQDNs fetched from the remote portal.
93+
// +optional
94+
FQDNCount int `json:"fqdnCount,omitempty"`
5095
}
5196

5297
// +kubebuilder:object:root=true
5398
// +kubebuilder:subresource:status
5499
// +kubebuilder:resource:path=portals,scope=Namespaced
55100
// +kubebuilder:printcolumn:name="Title",type=string,JSONPath=`.spec.title`
56101
// +kubebuilder:printcolumn:name="Main",type=boolean,JSONPath=`.spec.main`
102+
// +kubebuilder:printcolumn:name="Remote URL",type=string,JSONPath=`.spec.remote.url`,priority=1
57103
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready`
58104
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp`
59105

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 45 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/main.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,14 @@ func main() {
8585
"Path to the operator configuration file.")
8686
flag.StringVar(&portalNamespace, "portal-namespace", "sreportal-system",
8787
"The namespace where the main portal will be auto-created.")
88-
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
88+
flag.StringVar(&probeAddr, "health-probe-bind-address", ":9090", "The address the probe endpoint binds to.")
8989
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
9090
"Enable leader election for controller manager. "+
9191
"Enabling this will ensure there is only one active controller manager.")
9292
flag.BoolVar(&secureMetrics, "metrics-secure", true,
9393
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
94+
var webhookPort int
95+
flag.IntVar(&webhookPort, "webhook-port", 9443, "The port the webhook server binds to.")
9496
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
9597
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
9698
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
@@ -135,6 +137,7 @@ func main() {
135137
webhookTLSOpts := tlsOpts
136138
webhookServerOptions := webhook.Options{
137139
TLSOpts: webhookTLSOpts,
140+
Port: webhookPort,
138141
}
139142

140143
if len(webhookCertPath) > 0 {
@@ -277,6 +280,10 @@ func main() {
277280
setupLog.Error(err, "unable to create webhook", "webhook", "DNS")
278281
os.Exit(1)
279282
}
283+
if err := webhookv1alpha1.SetupPortalWebhookWithManager(mgr); err != nil {
284+
setupLog.Error(err, "unable to create webhook", "webhook", "Portal")
285+
os.Exit(1)
286+
}
280287
}
281288
if err := controller.NewPortalReconciler(
282289
mgr.GetClient(),
@@ -289,6 +296,7 @@ func main() {
289296
// Add runnable to ensure main portal exists at startup
290297
if err := mgr.Add(portalctrl.NewEnsureMainPortalRunnable(
291298
mgr.GetClient(),
299+
mgr.GetCache(),
292300
portalNamespace,
293301
)); err != nil {
294302
setupLog.Error(err, "unable to add main portal ensure runnable")

config/crd/bases/sreportal.my.domain_dns.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,12 @@ spec:
191191
description: name is the group name
192192
type: string
193193
source:
194-
description: source indicates where this group came from (manual
195-
or external-dns)
194+
description: source indicates where this group came from (manual,
195+
external-dns, or remote)
196196
enum:
197197
- manual
198198
- external-dns
199+
- remote
199200
type: string
200201
required:
201202
- name

config/crd/bases/sreportal.my.domain_portals.yaml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ spec:
2121
- jsonPath: .spec.main
2222
name: Main
2323
type: boolean
24+
- jsonPath: .spec.remote.url
25+
name: Remote URL
26+
priority: 1
27+
type: string
2428
- jsonPath: .status.ready
2529
name: Ready
2630
type: boolean
@@ -56,6 +60,25 @@ spec:
5660
description: main marks this portal as the default portal for unmatched
5761
FQDNs
5862
type: boolean
63+
remote:
64+
description: |-
65+
remote configures this portal to fetch data from a remote SRE Portal instance.
66+
When set, the operator will fetch DNS information from the remote portal
67+
instead of collecting data from the local cluster.
68+
This field cannot be set when main is true.
69+
properties:
70+
portal:
71+
description: |-
72+
portal is the name of the portal to target on the remote instance.
73+
If not set, the main portal of the remote instance will be used.
74+
type: string
75+
url:
76+
description: url is the base URL of the remote SRE Portal instance.
77+
pattern: ^https?://.*
78+
type: string
79+
required:
80+
- url
81+
type: object
5982
subPath:
6083
description: subPath is the URL subpath for this portal (defaults
6184
to metadata.name)
@@ -134,6 +157,30 @@ spec:
134157
ready:
135158
description: ready indicates if the portal is fully configured
136159
type: boolean
160+
remoteSync:
161+
description: |-
162+
remoteSync contains the status of synchronization with a remote portal.
163+
This is only populated when spec.remote is set.
164+
properties:
165+
fqdnCount:
166+
description: fqdnCount is the number of FQDNs fetched from the
167+
remote portal.
168+
type: integer
169+
lastSyncError:
170+
description: |-
171+
lastSyncError contains the error message from the last failed synchronization attempt.
172+
Empty if the last sync was successful.
173+
type: string
174+
lastSyncTime:
175+
description: lastSyncTime is the timestamp of the last successful
176+
synchronization.
177+
format: date-time
178+
type: string
179+
remoteTitle:
180+
description: remoteTitle is the title of the remote portal as
181+
fetched from the remote server.
182+
type: string
183+
type: object
137184
type: object
138185
required:
139186
- spec

0 commit comments

Comments
 (0)