fix: replace client-go informer with REST loader, drop k8s deps
CI / Vet, test, build (push) Successful in 2m7s
CI / Build and push image (push) Successful in 51s

Informer cache sync timed out due to transient 429 from API server.
REST loader does one GET + retry, polls every 30s. Zero new deps.
go.mod back to 1.25.4, CI back to golang:1.25-bookworm.
This commit is contained in:
Admin Bot
2026-08-26 15:49:34 -07:00
parent c237b633e9
commit fbc197b471
7 changed files with 179 additions and 451 deletions
+1 -1
View File
@@ -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
+8 -24
View File
@@ -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"
)
+20 -122
View File
@@ -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.
+14 -49
View File
@@ -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)
}
+2 -40
View File
@@ -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
)
-94
View File
@@ -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=
+134 -121
View File
@@ -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"`
}