diff --git a/Dockerfile b/Dockerfile index 2a31ce9..5d7d6e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ # --platform=$BUILDPLATFORM pins the build stage to the machine doing the # building, then Go cross-compiles to $TARGETARCH. Without it, building an # amd64 image from an arm64 workstation runs the whole toolchain under QEMU. -FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS build WORKDIR /src diff --git a/apis/gateway/v1/groupversion_info.go b/apis/gateway/v1/groupversion_info.go index c560ef1..65368a0 100644 --- a/apis/gateway/v1/groupversion_info.go +++ b/apis/gateway/v1/groupversion_info.go @@ -1,29 +1,13 @@ -/* -Copyright 2026. - -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 v1 contains API Schema definitions for the gateway v1 API group +// Package v1 contains API Schema definitions for the gateway v1 API group. +// These types document the CRD shape; the gateway itself uses the dynamic +// REST loader (no typed client, no scheme registration). +// // +kubebuilder:object:generate=true // +groupName=gateway.riotpiao.com package v1 -import ( - "k8s.io/apimachinery/pkg/runtime/schema" -) - -var ( - // GroupVersion is group version used to register these objects. - GroupVersion = schema.GroupVersion{Group: "gateway.riotpiao.com", Version: "v1"} +// GroupVersion constants for the CRD. +const ( + Group = "gateway.riotpiao.com" + Version = "v1" ) diff --git a/apis/gateway/v1/serviceadapter_types.go b/apis/gateway/v1/serviceadapter_types.go index 8abae08..8ae56c2 100644 --- a/apis/gateway/v1/serviceadapter_types.go +++ b/apis/gateway/v1/serviceadapter_types.go @@ -1,160 +1,58 @@ -/* -Copyright 2026. - -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 v1 documents the ServiceAdapter CRD shape. +// These structs mirror the CRD schema in k8s/crd-serviceadapter.yaml. +// The gateway reads CRs via plain REST+JSON — no typed client needed. package v1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - // ServiceAdapterUpstream defines the upstream target for this adapter. type ServiceAdapterUpstream struct { - // URL is the upstream service endpoint. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - URL string `json:"url"` - - // TimeoutSeconds is the request timeout in seconds. - // +kubebuilder:validation:Required - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=3600 - TimeoutSeconds int32 `json:"timeoutSeconds"` + URL string `json:"url"` + TimeoutSeconds int32 `json:"timeoutSeconds"` } // ServiceAdapterAuth defines authentication requirements. type ServiceAdapterAuth struct { - // Required indicates if authentication is needed. - // +kubebuilder:validation:Required - Required bool `json:"required"` - - // Capability is the required capability name (e.g., "reasoning", "embedding"). - // Empty if auth is not required. - // +kubebuilder:validation:Optional + Required bool `json:"required"` Capability string `json:"capability,omitempty"` } // ServiceAdapterMethod defines a single method endpoint. type ServiceAdapterMethod struct { - // Verb is the HTTP method (GET, POST, etc.). - // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=GET;POST;PUT;DELETE;PATCH;HEAD;OPTIONS - Verb string `json:"verb"` - - // UpstreamPath is the path to forward to on the upstream. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - UpstreamPath string `json:"upstreamPath"` - - // RequestSchema is the flat KV+type validation schema for requests (optional). - // +kubebuilder:validation:Optional - RequestSchema string `json:"requestSchema,omitempty"` - - // ResponseSchema is the flat KV+type validation schema for responses (optional). - // +kubebuilder:validation:Optional - ResponseSchema string `json:"responseSchema,omitempty"` - - // Auth overrides the resource-level auth for this method (optional). - // +kubebuilder:validation:Optional - Auth *ServiceAdapterAuth `json:"auth,omitempty"` + Verb string `json:"verb"` + UpstreamPath string `json:"upstreamPath"` + RequestSchema string `json:"requestSchema,omitempty"` + ResponseSchema string `json:"responseSchema,omitempty"` + Auth *ServiceAdapterAuth `json:"auth,omitempty"` } // ServiceAdapterResource defines a resource exposed by this adapter. type ServiceAdapterResource struct { - // Name is the resource identifier. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - Name string `json:"name"` - - // Methods are the HTTP methods available for this resource. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems=1 + Name string `json:"name"` Methods []ServiceAdapterMethod `json:"methods"` - - // Auth applies to all methods in this resource unless overridden. - // +kubebuilder:validation:Optional - Auth *ServiceAdapterAuth `json:"auth,omitempty"` + Auth *ServiceAdapterAuth `json:"auth,omitempty"` } // ServiceAdapterSpec defines the desired state of ServiceAdapter. type ServiceAdapterSpec struct { - // ServiceName is the unique identifier for this service. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=63 - ServiceName string `json:"serviceName"` - - // Upstream defines where to forward requests. - // +kubebuilder:validation:Required - Upstream ServiceAdapterUpstream `json:"upstream"` - - // Auth defines default authentication for this adapter. - // +kubebuilder:validation:Required - Auth ServiceAdapterAuth `json:"auth"` - - // Retryable indicates if requests can be retried on 5xx. - // +kubebuilder:validation:Optional - // +kubebuilder:validation:Default=false - Retryable bool `json:"retryable,omitempty"` - - // Resources are the endpoints exposed by this adapter. - // +kubebuilder:validation:Required - Resources []ServiceAdapterResource `json:"resources"` + ServiceName string `json:"serviceName"` + Upstream ServiceAdapterUpstream `json:"upstream"` + Auth ServiceAdapterAuth `json:"auth"` + Retryable bool `json:"retryable,omitempty"` + Resources []ServiceAdapterResource `json:"resources"` } // ServiceAdapterStatus defines the observed state of ServiceAdapter. type ServiceAdapterStatus struct { - // Ready indicates if the adapter is loaded and healthy. - // +kubebuilder:validation:Optional - Ready bool `json:"ready,omitempty"` - - // Error message if the adapter failed to load. - // +kubebuilder:validation:Optional + Ready bool `json:"ready,omitempty"` Error string `json:"error,omitempty"` - - // LastSyncTime is when the adapter was last synced. - // +kubebuilder:validation:Optional - LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` } -// +kubebuilder:object:root=true -// +kubebuilder:resource:scope=Namespaced -// +kubebuilder:subresource:status -// +kubebuilder:printcolumn:name="Service",type=string,JSONPath=`.spec.serviceName` -// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready` -// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` - // ServiceAdapter describes a service exposed through the gateway. type ServiceAdapter struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ServiceAdapterSpec `json:"spec,omitempty"` Status ServiceAdapterStatus `json:"status,omitempty"` } -// +kubebuilder:object:root=true - // ServiceAdapterList contains a list of ServiceAdapter. type ServiceAdapterList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []ServiceAdapter `json:"items"` + Items []ServiceAdapter `json:"items"` } - -// Note: DeepCopyObject is not generated. The gateway uses the dynamic -// (unstructured) k8s client, so typed scheme registration is unnecessary. -// If a typed client is needed later, run controller-gen to generate -// the DeepCopy methods and re-enable SchemeBuilder.Register here. diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index e13bae4..3783b89 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -1,17 +1,13 @@ package main import ( - "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" - - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" - "k8s.io/klog/v2" + "time" "forgejo.riotpiao.com/rock/homelab-frontend/internal/config" "forgejo.riotpiao.com/rock/homelab-frontend/internal/proxy" @@ -21,14 +17,12 @@ import ( ) func main() { - // Load configuration cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "failed to load config: %v\n", err) os.Exit(1) } - // Determine if auth is enabled by checking if any route requires it authEnabled := false for _, route := range cfg.Routes { if route.Upstream.AuthRequired { @@ -37,10 +31,8 @@ func main() { } } - // Create the reverse proxy handler that routes requests based on configuration upstreamHandler := proxy.New(cfg) - // Create the Temporal workflow handler temporalHostPort := os.Getenv("TEMPORAL_HOST_PORT") if temporalHostPort == "" { temporalHostPort = "localhost:7233" @@ -48,77 +40,50 @@ func main() { log.Printf("Temporal server: %s", temporalHostPort) temporalHandler := temporal.NewHandler(temporalHostPort) - // Create server with health checker srv := server.New(cfg.ListenAddr, cfg.ShutdownTimeout, nil) - - // Initialize health checker with config validity and auth status healthChecker := server.NewHealthChecker(true, authEnabled) srv.SetHealthChecker(healthChecker) - // Route klog (used by k8s client-go) to stderr so reflector errors are visible - klog.InitFlags(nil) - klog.SetOutput(os.Stderr) - - // Create ServiceAdapter registry and dispatcher (phase 8) + // ServiceAdapter: registry + loader + dispatcher registry := serviceadapter.NewRegistry(nil) dispatcher := serviceadapter.NewDispatcher(registry) - // Start ServiceAdapter informer to watch CRDs (8.1 integration) - var informerMgr *serviceadapter.InformerManager - kubeConfig, err := rest.InClusterConfig() - if err != nil { - log.Printf("ServiceAdapter informer disabled: %v", err) + var loader *serviceadapter.Loader + if l, err := serviceadapter.NewLoader(registry, "api"); err != nil { + log.Printf("ServiceAdapter loader disabled: %v", err) } else { - dynamicClient, err := dynamic.NewForConfig(kubeConfig) - if err != nil { - log.Printf("ServiceAdapter informer: failed to create k8s client: %v", err) + if err := l.Start(30 * time.Second); err != nil { + log.Printf("ServiceAdapter loader failed: %v", err) } else { - mgr, err := serviceadapter.NewInformerManager(dynamicClient, registry, "api") - if err != nil { - log.Printf("ServiceAdapter informer: failed to create manager: %v", err) - } else { - if err := mgr.Start(context.Background()); err != nil { - log.Printf("ServiceAdapter informer: failed to start: %v", err) - } else { - informerMgr = mgr - log.Printf("ServiceAdapter informer started") - } - } + loader = l + log.Printf("ServiceAdapter loader started") } } - // Create router that handles health endpoints, X-Service (ServiceAdapter) routing, - // temporal endpoints, and passes others to upstream handler router := server.NewRouter(healthChecker, dispatcher, temporalHandler, upstreamHandler) srv.SetHandler(router) - // Set up signal handling sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT) - // Start server in a goroutine - var serverErr error go func() { log.Printf("gateway listening on %s", srv.Addr()) - serverErr = srv.ListenAndServe() - if serverErr != nil && serverErr != http.ErrServerClosed { - log.Printf("server error: %v", serverErr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("server error: %v", err) } }() - // Wait for shutdown signal sig := <-sigChan log.Printf("received signal: %v", sig) - if informerMgr != nil { - informerMgr.Stop() + if loader != nil { + loader.Stop() } - if err := srv.Shutdown(context.Background()); err != nil { + if err := srv.Shutdown(nil); err != nil { fmt.Fprintf(os.Stderr, "shutdown error: %v\n", err) os.Exit(1) } log.Printf("gateway shutdown complete") - os.Exit(0) } diff --git a/go.mod b/go.mod index 7d909a8..851e523 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module forgejo.riotpiao.com/rock/homelab-frontend -go 1.26.0 +go 1.25.4 require ( go.temporal.io/api v1.63.5 @@ -8,66 +8,28 @@ require ( google.golang.org/grpc v1.83.1 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 - k8s.io/apimachinery v0.37.0 - k8s.io/client-go v0.37.0 - k8s.io/klog/v2 v2.140.0 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect - github.com/fxamacker/cbor/v2 v2.9.1 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-openapi/jsonpointer v1.0.0 // indirect - github.com/go-openapi/jsonreference v1.0.0 // indirect - github.com/go-openapi/swag v0.27.1 // indirect - github.com/go-openapi/swag/cmdutils v0.27.1 // indirect - github.com/go-openapi/swag/conv v0.27.1 // indirect - github.com/go-openapi/swag/fileutils v0.27.1 // indirect - github.com/go-openapi/swag/jsonutils v0.27.1 // indirect - github.com/go-openapi/swag/loading v0.27.1 // indirect - github.com/go-openapi/swag/mangling v0.27.1 // indirect - github.com/go-openapi/swag/netutils v0.27.1 // indirect - github.com/go-openapi/swag/pools v0.27.1 // indirect - github.com/go-openapi/swag/stringutils v0.27.1 // indirect - github.com/go-openapi/swag/typeutils v0.27.1 // indirect - github.com/go-openapi/swag/yamlutils v0.27.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.6.0 // indirect - github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/nexus-rpc/nexus-proto-annotations v0.1.0 // indirect github.com/nexus-rpc/sdk-go v0.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/robfig/cron v1.2.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/testify v1.11.1 // indirect - github.com/x448/float16 v0.8.4 // indirect - go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.57.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/term v0.45.0 // indirect golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - k8s.io/api v0.37.0 // indirect - k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect - k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect - sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect - sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 6f4dfac..ed3e76d 100644 --- a/go.sum +++ b/go.sum @@ -1,107 +1,47 @@ 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/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.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= -github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= -github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= -github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s= -github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y= -github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY= -github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0= -github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg= -github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE= -github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE= -github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM= -github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU= -github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs= -github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM= -github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8= -github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU= -github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY= -github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY= -github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE= -github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA= -github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w= -github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU= -github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ= -github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E= -github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE= -github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8= -github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM= -github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc= -github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ= -github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA= -github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0= -github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo= -github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug= -github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -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/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/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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/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/nexus-rpc/nexus-proto-annotations v0.1.0 h1:2fELd+9sqUtNu6Fg//pw8YFsxOvp8vZ8hfP0nHhNI80= github.com/nexus-rpc/nexus-proto-annotations v0.1.0/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y= github.com/nexus-rpc/sdk-go v0.7.0 h1:38NrfY5rLnZAiMMs2ZfCKI/CSDzdfJG+27iAgfA8bUI= github.com/nexus-rpc/sdk-go v0.7.0/go.mod h1:FHdPfVQwRuJFZFTF0Y2GOAxCrbIBNrcPna9slkGKPYk= -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/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 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/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -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= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -121,12 +61,6 @@ go.temporal.io/api v1.63.5 h1:c11+kPYHkXXL3UiShPdbMD+xtvqGsbTibUA9ypmiCa4= go.temporal.io/api v1.63.5/go.mod h1:SrlW2JMwVlDP4nRWSNznUFqnSHd+YeMDS1BkYo63HCQ= go.temporal.io/sdk v1.48.0 h1:WDctKDVuh0Z8Nf7euAyqs/EwcPg1JTIIq1Fut8Tq118= go.temporal.io/sdk v1.48.0/go.mod h1:SHv3+fLzD0GGZAwf0xNSvu8UmO1nFgG9WBSYoowApIk= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= -go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -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= @@ -140,8 +74,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= 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= @@ -157,8 +89,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= 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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= @@ -187,29 +117,5 @@ google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+ 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= -gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= -gopkg.in/evanphx/json-patch.v4 v4.13.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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4= -k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk= -k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0= -k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE= -k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ= -k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0= -k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= -k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= -k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= -k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs= -k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q= -sigs.k8s.io/structured-merge-diff/v6 v6.4.2/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/serviceadapter/informer.go b/internal/serviceadapter/informer.go index 9767e27..81090af 100644 --- a/internal/serviceadapter/informer.go +++ b/internal/serviceadapter/informer.go @@ -1,28 +1,33 @@ package serviceadapter import ( - "context" + "crypto/tls" + "crypto/x509" + "encoding/json" "fmt" + "io" "log" + "net/http" + "os" + "sync" "time" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/dynamic/dynamicinformer" - "k8s.io/client-go/tools/cache" ) -// InformerManager watches ServiceAdapter CRs and populates the registry. -type InformerManager struct { - registry *Registry - informer cache.SharedIndexInformer - stopChan chan struct{} - factory dynamicinformer.DynamicSharedInformerFactory +// Loader watches ServiceAdapter CRs via the Kubernetes REST API and populates the registry. +// No client-go dependency — uses the in-cluster service account token directly. +type Loader struct { + registry *Registry + namespace string + client *http.Client + token string + baseURL string + stopChan chan struct{} + once sync.Once } -// NewInformerManager creates a new informer that watches ServiceAdapters in the given namespace. -func NewInformerManager(dynamicClient dynamic.Interface, registry *Registry, namespace string) (*InformerManager, error) { +// NewLoader creates a loader that reads ServiceAdapters from the k8s API. +// Returns an error if not running inside a cluster (missing SA token/ca). +func NewLoader(registry *Registry, namespace string) (*Loader, error) { if registry == nil { return nil, fmt.Errorf("registry cannot be nil") } @@ -30,134 +35,142 @@ func NewInformerManager(dynamicClient dynamic.Interface, registry *Registry, nam namespace = "api" } - gvr := schema.GroupVersionResource{ - Group: "gateway.riotpiao.com", - Version: "v1", - Resource: "serviceadapters", - } - - factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory( - dynamicClient, 30*time.Second, namespace, nil, - ) - - informer := factory.ForResource(gvr).Informer() - - _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - u := obj.(*unstructured.Unstructured) - if a, err := parseServiceAdapter(u); err == nil { - if registry.Add(a) == nil { - log.Printf("serviceadapter added: %s", a.ServiceName) - } - } else { - log.Printf("serviceadapter parse error on add: %v", err) - } - }, - UpdateFunc: func(_, newObj interface{}) { - u := newObj.(*unstructured.Unstructured) - if a, err := parseServiceAdapter(u); err == nil { - registry.Update(a) - } - }, - DeleteFunc: func(obj interface{}) { - u, ok := obj.(*unstructured.Unstructured) - if !ok { - tombstone, ok := obj.(cache.DeletedFinalStateUnknown) - if !ok { - return - } - u, ok = tombstone.Obj.(*unstructured.Unstructured) - if !ok { - return - } - } - if name, ok := u.Object["metadata"].(map[string]interface{})["name"]; ok { - registry.Delete(name.(string)) - log.Printf("serviceadapter deleted: %s", name) - } - }, - }) + tokenBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") if err != nil { - return nil, fmt.Errorf("adding event handler: %w", err) + return nil, fmt.Errorf("not in cluster: %w", err) } - return &InformerManager{ - registry: registry, - informer: informer, + caBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt") + if err != nil { + return nil, fmt.Errorf("missing CA cert: %w", err) + } + + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(caBytes) + + return &Loader{ + registry: registry, + namespace: namespace, + token: string(tokenBytes), + baseURL: "https://kubernetes.default.svc", + client: &http.Client{ + Timeout: 10 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{RootCAs: pool}, + }, + }, stopChan: make(chan struct{}), - factory: factory, }, nil } -// Start begins watching ServiceAdapter resources. Blocks until cache syncs or timeout. -func (im *InformerManager) Start(ctx context.Context) error { - im.factory.Start(im.stopChan) - - done := make(chan bool, 1) - go func() { done <- cache.WaitForCacheSync(im.stopChan, im.informer.HasSynced) }() - - select { - case synced := <-done: - if !synced { - return fmt.Errorf("cache sync failed") +// Start loads ServiceAdapters immediately, then polls every interval. +func (l *Loader) Start(interval time.Duration) error { + if err := l.load(); err != nil { + // Retry once after 2s — handles transient "storage reinitializing" (429) + log.Printf("serviceadapter loader: first attempt failed (%v), retrying in 2s", err) + time.Sleep(2 * time.Second) + if err := l.load(); err != nil { + return fmt.Errorf("serviceadapter loader: %w", err) } - case <-time.After(30 * time.Second): - return fmt.Errorf("cache sync timed out") } + + // Background poll for changes + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + if err := l.load(); err != nil { + log.Printf("serviceadapter loader poll error: %v", err) + } + case <-l.stopChan: + return + } + } + }() + return nil } -// Stop stops the informer. -func (im *InformerManager) Stop() { - close(im.stopChan) +// Stop stops the background poll. +func (l *Loader) Stop() { + l.once.Do(func() { close(l.stopChan) }) } -// parseServiceAdapter converts an unstructured object to ServiceAdapter. -func parseServiceAdapter(u *unstructured.Unstructured) (*ServiceAdapter, error) { - metadata, ok := u.Object["metadata"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("invalid metadata") +// load fetches all ServiceAdapters from the k8s API and syncs the registry. +func (l *Loader) load() error { + url := fmt.Sprintf("%s/apis/gateway.riotpiao.com/v1/namespaces/%s/serviceadapters", l.baseURL, l.namespace) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err } - name, _ := metadata["name"].(string) - namespace, _ := metadata["namespace"].(string) - if namespace == "" { - namespace = "default" + req.Header.Set("Authorization", "Bearer "+l.token) + req.Header.Set("Accept", "application/json") + + resp, err := l.client.Do(req) + if err != nil { + return fmt.Errorf("k8s API request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == 429 { + return fmt.Errorf("k8s API: storage reinitializing (429)") + } + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("k8s API: %d %s", resp.StatusCode, string(body[:min(len(body), 200)])) } - spec, ok := u.Object["spec"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("missing spec") + var list crList + if err := json.NewDecoder(resp.Body).Decode(&list); err != nil { + return fmt.Errorf("decoding response: %w", err) } - serviceName, ok := spec["serviceName"].(string) - if !ok { - return nil, fmt.Errorf("missing spec.serviceName") - } - - adapter := &ServiceAdapter{ - Name: name, - Namespace: namespace, - ServiceName: serviceName, - CreatedAt: time.Now(), - } - - if upstreamMap, ok := spec["upstream"].(map[string]interface{}); ok { - if url, ok := upstreamMap["url"].(string); ok { - adapter.Spec.Upstream.URL = url + // Sync: add/update all found, registry handles dedup + for i := range list.Items { + item := &list.Items[i] + adapter := &ServiceAdapter{ + Name: item.Metadata.Name, + Namespace: item.Metadata.Namespace, + ServiceName: item.Spec.ServiceName, + CreatedAt: time.Now(), } - if timeout, ok := upstreamMap["timeoutSeconds"].(float64); ok { - adapter.Spec.Upstream.TimeoutSeconds = int32(timeout) + adapter.Spec.Upstream.URL = item.Spec.Upstream.URL + adapter.Spec.Upstream.TimeoutSeconds = item.Spec.Upstream.TimeoutSeconds + adapter.Spec.Auth.Required = item.Spec.Auth.Required + adapter.Spec.Auth.Capability = item.Spec.Auth.Capability + + if existing := l.registry.Get(item.Spec.ServiceName); existing != nil { + l.registry.Update(adapter) + } else { + l.registry.Add(adapter) + log.Printf("serviceadapter loaded: %s → %s", item.Spec.ServiceName, item.Spec.Upstream.URL) } } - if authMap, ok := spec["auth"].(map[string]interface{}); ok { - if required, ok := authMap["required"].(bool); ok { - adapter.Spec.Auth.Required = required - } - if capability, ok := authMap["capability"].(string); ok { - adapter.Spec.Auth.Capability = capability - } - } + return nil +} - return adapter, nil +// Minimal JSON structs for the k8s list response — no client-go needed. +type crList struct { + Items []crItem `json:"items"` +} + +type crItem struct { + Metadata struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + } `json:"metadata"` + Spec struct { + ServiceName string `json:"serviceName"` + Upstream struct { + URL string `json:"url"` + TimeoutSeconds int32 `json:"timeoutSeconds"` + } `json:"upstream"` + Auth struct { + Required bool `json:"required"` + Capability string `json:"capability"` + } `json:"auth"` + } `json:"spec"` }