Skip to content

Commit d7fe133

Browse files
committed
feat: implement state manager
1 parent d89b92c commit d7fe133

File tree

7 files changed

+220
-98
lines changed

7 files changed

+220
-98
lines changed

internal/config/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ type Config struct {
1010
Namespace string
1111
GatewayName string
1212
IngressClass string
13-
OutputDir string
13+
Output string
1414
DefaultInterval time.Duration
1515
DefaultDNSResolver string
1616
DefaultCondition string
@@ -23,7 +23,7 @@ func Load() *Config {
2323
flag.StringVar(&cfg.Namespace, "namespace", "", "Namespace to watch (empty for all)")
2424
flag.StringVar(&cfg.GatewayName, "gateway", "", "Gateway name to filter HTTPRoutes (required for HTTPRoute mode)")
2525
flag.StringVar(&cfg.IngressClass, "ingress-class", "", "Ingress class to filter Ingresses (optional for Ingress mode)")
26-
flag.StringVar(&cfg.OutputDir, "output", "/config", "Directory to write generated YAML files")
26+
flag.StringVar(&cfg.Output, "output", "/config/gatus-sidecar.yaml", "File to write generated YAML")
2727
flag.DurationVar(&cfg.DefaultInterval, "default-interval", time.Minute, "Default interval value for endpoints")
2828
flag.StringVar(&cfg.DefaultDNSResolver, "default-dns", "tcp://1.1.1.1:53", "Default DNS resolver for endpoints")
2929
flag.StringVar(&cfg.DefaultCondition, "default-condition", "[STATUS] == 200", "Default condition")

internal/controller/controller.go

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,29 @@ import (
2020
"k8s.io/client-go/rest"
2121

2222
"github.com/home-operations/gatus-sidecar/internal/config"
23-
"github.com/home-operations/gatus-sidecar/internal/generator"
23+
"github.com/home-operations/gatus-sidecar/internal/endpoint"
2424
"github.com/home-operations/gatus-sidecar/internal/handler"
25+
"github.com/home-operations/gatus-sidecar/internal/state"
2526
)
2627

2728
// Controller is a generic Kubernetes resource controller
2829
type Controller struct {
29-
gvr schema.GroupVersionResource
30-
handler handler.ResourceHandler
31-
convert func(*unstructured.Unstructured) (metav1.Object, error)
30+
gvr schema.GroupVersionResource
31+
handler handler.ResourceHandler
32+
convert func(*unstructured.Unstructured) (metav1.Object, error)
33+
stateManager *state.Manager
3234
}
3335

3436
// NewIngressController creates a controller for Ingress resources
35-
func NewIngressController(resourceHandler handler.ResourceHandler) *Controller {
37+
func NewIngressController(resourceHandler handler.ResourceHandler, stateManager *state.Manager) *Controller {
3638
return &Controller{
3739
gvr: schema.GroupVersionResource{
3840
Group: "networking.k8s.io",
3941
Version: "v1",
4042
Resource: "ingresses",
4143
},
42-
handler: resourceHandler,
44+
handler: resourceHandler,
45+
stateManager: stateManager,
4346
convert: func(u *unstructured.Unstructured) (metav1.Object, error) {
4447
ingress := &networkingv1.Ingress{}
4548
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, ingress); err != nil {
@@ -51,10 +54,11 @@ func NewIngressController(resourceHandler handler.ResourceHandler) *Controller {
5154
}
5255

5356
// NewHTTPRouteController creates a controller for HTTPRoute resources
54-
func NewHTTPRouteController(resourceHandler handler.ResourceHandler) *Controller {
57+
func NewHTTPRouteController(resourceHandler handler.ResourceHandler, stateManager *state.Manager) *Controller {
5558
return &Controller{
56-
gvr: gatewayv1.SchemeGroupVersion.WithResource("httproutes"),
57-
handler: resourceHandler,
59+
gvr: gatewayv1.SchemeGroupVersion.WithResource("httproutes"),
60+
handler: resourceHandler,
61+
stateManager: stateManager,
5862
convert: func(u *unstructured.Unstructured) (metav1.Object, error) {
5963
route := &gatewayv1.HTTPRoute{}
6064
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, route); err != nil {
@@ -133,21 +137,21 @@ func (c *Controller) handleEvent(cfg *config.Config, obj metav1.Object, eventTyp
133137
}
134138

135139
name := obj.GetName()
136-
filename := fmt.Sprintf("%s-%s.yaml", obj.GetName(), obj.GetNamespace())
140+
namespace := obj.GetNamespace()
141+
resource := fmt.Sprintf("%s-%s", name, namespace)
137142

138143
if eventType == watch.Deleted {
139-
if err := generator.Delete(cfg.OutputDir, filename); err != nil {
140-
slog.Error("failed to delete file for resource", c.handler.GetResourceName(), obj.GetName(), "error", err)
141-
} else {
142-
slog.Info("deleted file for resource", c.handler.GetResourceName(), obj.GetName())
144+
changed := c.stateManager.Remove(resource)
145+
if changed {
146+
slog.Info("removed endpoint from state", "resource", c.handler.GetResourceName(), "name", name)
143147
}
144148
return
145149
}
146150

147151
// Get the URL from the resource
148152
url := c.handler.ExtractURL(obj)
149153
if url == "" {
150-
slog.Warn("resource has no hosts/hostnames", c.handler.GetResourceName(), obj.GetName())
154+
slog.Warn("resource has no hosts/hostnames", "resource", c.handler.GetResourceName(), "name", name)
151155
return
152156
}
153157

@@ -161,24 +165,29 @@ func (c *Controller) handleEvent(cfg *config.Config, obj metav1.Object, eventTyp
161165
if annotations != nil {
162166
if templateStr, ok := annotations[cfg.TemplateAnnotation]; ok && templateStr != "" {
163167
if err := yaml.Unmarshal([]byte(templateStr), &templateData); err != nil {
164-
slog.Error("failed to unmarshal template for resource", c.handler.GetResourceName(), obj.GetName(), "error", err)
168+
slog.Error("failed to unmarshal template for resource", "resource", c.handler.GetResourceName(), "name", name, "error", err)
165169
return
166170
}
167171
}
168172
}
169173

170-
data := map[string]any{
171-
"name": name,
172-
"url": url,
173-
"interval": interval,
174-
"client": map[string]any{"dns-resolver": dnsResolver},
175-
"conditions": []string{condition},
174+
// Create endpoint state with defaults
175+
endpoint := &endpoint.Endpoint{
176+
Name: name,
177+
URL: url,
178+
Interval: interval,
179+
Client: map[string]any{"dns-resolver": dnsResolver},
180+
Conditions: []string{condition},
176181
}
177182

178-
// Write with optional template data
179-
if err := generator.Write(data, cfg.OutputDir, filename, templateData); err != nil {
180-
slog.Error("write file for resource", c.handler.GetResourceName(), obj.GetName(), "error", err)
181-
} else {
182-
slog.Info("wrote file for resource", c.handler.GetResourceName(), obj.GetName())
183+
// Apply template overrides if present
184+
if templateData != nil {
185+
endpoint.ApplyTemplate(templateData)
186+
}
187+
188+
// Update state
189+
changed := c.stateManager.AddOrUpdate(resource, endpoint)
190+
if changed {
191+
slog.Info("updated endpoint in state", "resource", c.handler.GetResourceName(), "name", name)
183192
}
184193
}

internal/controller/httproute.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/home-operations/gatus-sidecar/internal/config"
1111
"github.com/home-operations/gatus-sidecar/internal/handler"
12+
"github.com/home-operations/gatus-sidecar/internal/state"
1213
)
1314

1415
// HTTPRouteHandler handles HTTPRoute resources
@@ -69,7 +70,8 @@ func referencesGateway(route *gatewayv1.HTTPRoute, gatewayName string) bool {
6970
}
7071

7172
func RunHTTPRoute(ctx context.Context, cfg *config.Config) error {
73+
stateManager := state.NewManager(cfg.Output)
7274
handler := &HTTPRouteHandler{}
73-
ctrl := NewHTTPRouteController(handler)
75+
ctrl := NewHTTPRouteController(handler, stateManager)
7476
return ctrl.Run(ctx, cfg)
7577
}

internal/controller/ingress.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/home-operations/gatus-sidecar/internal/config"
1313
"github.com/home-operations/gatus-sidecar/internal/handler"
14+
"github.com/home-operations/gatus-sidecar/internal/state"
1415
)
1516

1617
// IngressHandler handles Ingress resources
@@ -94,7 +95,8 @@ func hasIngressClass(ingress *networkingv1.Ingress, ingressClass string) bool {
9495
}
9596

9697
func RunIngress(ctx context.Context, cfg *config.Config) error {
98+
stateManager := state.NewManager(cfg.Output)
9799
handler := &IngressHandler{}
98-
ctrl := NewIngressController(handler)
100+
ctrl := NewIngressController(handler, stateManager)
99101
return ctrl.Run(ctx, cfg)
100102
}

internal/endpoint/endpoint.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package endpoint
2+
3+
import "maps"
4+
5+
// Endpoint represents the configuration for a single endpoint
6+
type Endpoint struct {
7+
Name string `yaml:"name"`
8+
URL string `yaml:"url"`
9+
Interval string `yaml:"interval"`
10+
Client map[string]any `yaml:"client"`
11+
Conditions []string `yaml:"conditions"`
12+
Extra map[string]any `yaml:",inline,omitempty"` // For additional template fields
13+
}
14+
15+
// ApplyTemplate applies template data to the endpoint, allowing overrides of default values
16+
func (e *Endpoint) ApplyTemplate(templateData map[string]any) {
17+
if templateData == nil {
18+
return
19+
}
20+
21+
// Initialize Extra map if needed
22+
if e.Extra == nil {
23+
e.Extra = make(map[string]any)
24+
}
25+
26+
// Apply template overrides
27+
for key, value := range templateData {
28+
switch key {
29+
case "name":
30+
e.setStringField(&e.Name, value)
31+
case "url":
32+
e.setStringField(&e.URL, value)
33+
case "interval":
34+
e.setStringField(&e.Interval, value)
35+
case "client":
36+
e.setClientField(value)
37+
case "conditions":
38+
e.setConditionsField(value)
39+
default:
40+
// Store other fields in Extra for inline YAML output
41+
e.Extra[key] = value
42+
}
43+
}
44+
}
45+
46+
// setStringField sets a string field if the value is a string
47+
func (e *Endpoint) setStringField(field *string, value any) {
48+
if str, ok := value.(string); ok {
49+
*field = str
50+
}
51+
}
52+
53+
// setClientField merges client settings
54+
func (e *Endpoint) setClientField(value any) {
55+
if client, ok := value.(map[string]any); ok {
56+
if e.Client == nil {
57+
e.Client = make(map[string]any)
58+
}
59+
maps.Copy(e.Client, client)
60+
}
61+
}
62+
63+
// setConditionsField handles different condition formats
64+
func (e *Endpoint) setConditionsField(value any) {
65+
switch v := value.(type) {
66+
case []string:
67+
e.Conditions = v
68+
case []any:
69+
conditions := make([]string, 0, len(v))
70+
for _, cond := range v {
71+
if str, ok := cond.(string); ok {
72+
conditions = append(conditions, str)
73+
}
74+
}
75+
e.Conditions = conditions
76+
case string:
77+
e.Conditions = []string{v}
78+
}
79+
}

internal/generator/generator.go

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)