This is an automated email from the ASF dual-hosted git repository.
ashishtiwari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new b47ed044 feat: add unified API server with debugging capabilities
(#2550)
b47ed044 is described below
commit b47ed044d6b28dd183a25f6eb79d3aaf88ffe8fe
Author: Ashish Tiwari <[email protected]>
AuthorDate: Thu Sep 18 11:06:46 2025 +0530
feat: add unified API server with debugging capabilities (#2550)
---
config/samples/config.yaml | 2 +
internal/adc/client/client.go | 15 +-
internal/controller/config/config.go | 1 +
internal/controller/config/types.go | 3 +
internal/manager/run.go | 9 +
internal/manager/server/server.go | 69 ++++++
internal/provider/apisix/provider.go | 6 +-
internal/provider/common/adcdebugserver.go | 341 +++++++++++++++++++++++++++++
internal/provider/provider.go | 1 +
internal/provider/register.go | 5 +
10 files changed, 445 insertions(+), 7 deletions(-)
diff --git a/config/samples/config.yaml b/config/samples/config.yaml
index 6d2246f7..a8e663fd 100644
--- a/config/samples/config.yaml
+++ b/config/samples/config.yaml
@@ -18,6 +18,8 @@ leader_election:
metrics_addr: ":8080" # The address the metrics endpoint
binds to.
# The default value is ":8080".
+enable_server: false # The debug API is behind this server
which is disabled by default for security reasons.
+server_addr: "127.0.0.1:9092" # Available endpoints: /debug can be
used to debug in-memory state of translated adc configs to be synced with data
plane.
enable_http2: false # Whether to enable HTTP/2 for the
server.
# The default value is false.
diff --git a/internal/adc/client/client.go b/internal/adc/client/client.go
index d918f8ba..3474636d 100644
--- a/internal/adc/client/client.go
+++ b/internal/adc/client/client.go
@@ -45,7 +45,8 @@ type Client struct {
executor ADCExecutor
BackendMode string
- ConfigManager *common.ConfigManager[types.NamespacedNameKind,
adctypes.Config]
+ ConfigManager *common.ConfigManager[types.NamespacedNameKind,
adctypes.Config]
+ ADCDebugProvider *common.ADCDebugProvider
}
func New(mode string, timeout time.Duration) (*Client, error) {
@@ -53,13 +54,15 @@ func New(mode string, timeout time.Duration) (*Client,
error) {
if serverURL == "" {
serverURL = defaultHTTPADCExecutorAddr
}
-
+ store := cache.NewStore()
+ configManager := common.NewConfigManager[types.NamespacedNameKind,
adctypes.Config]()
log.Infow("using HTTP ADC Executor", zap.String("server_url",
serverURL))
return &Client{
- Store: cache.NewStore(),
- executor: NewHTTPADCExecutor(serverURL, timeout),
- BackendMode: mode,
- ConfigManager:
common.NewConfigManager[types.NamespacedNameKind, adctypes.Config](),
+ Store: store,
+ executor: NewHTTPADCExecutor(serverURL, timeout),
+ BackendMode: mode,
+ ConfigManager: configManager,
+ ADCDebugProvider: common.NewADCDebugProvider(store,
configManager),
}, nil
}
diff --git a/internal/controller/config/config.go
b/internal/controller/config/config.go
index d041b123..c6f35271 100644
--- a/internal/controller/config/config.go
+++ b/internal/controller/config/config.go
@@ -48,6 +48,7 @@ func NewDefaultConfig() *Config {
LeaderElectionID: DefaultLeaderElectionID,
ProbeAddr: DefaultProbeAddr,
MetricsAddr: DefaultMetricsAddr,
+ ServerAddr: DefaultServerAddr,
LeaderElection: NewLeaderElection(),
ExecADCTimeout: types.TimeDuration{Duration: 15 *
time.Second},
ProviderConfig: ProviderConfig{
diff --git a/internal/controller/config/types.go
b/internal/controller/config/types.go
index dfe1dd32..997343ce 100644
--- a/internal/controller/config/types.go
+++ b/internal/controller/config/types.go
@@ -43,6 +43,7 @@ const (
DefaultMetricsAddr = ":8080"
DefaultProbeAddr = ":8081"
+ DefaultServerAddr = ":9092"
)
// Config contains all config items which are necessary for
@@ -52,6 +53,8 @@ type Config struct {
ControllerName string `json:"controller_name"
yaml:"controller_name"`
LeaderElectionID string `json:"leader_election_id"
yaml:"leader_election_id"`
MetricsAddr string `json:"metrics_addr"
yaml:"metrics_addr"`
+ ServerAddr string `json:"server_addr"
yaml:"server_addr"`
+ EnableServer bool `json:"enable_server"
yaml:"enable_server"`
EnableHTTP2 bool `json:"enable_http2"
yaml:"enable_http2"`
ProbeAddr string `json:"probe_addr"
yaml:"probe_addr"`
SecureMetrics bool `json:"secure_metrics"
yaml:"secure_metrics"`
diff --git a/internal/manager/run.go b/internal/manager/run.go
index c08ccaba..caea487d 100644
--- a/internal/manager/run.go
+++ b/internal/manager/run.go
@@ -42,6 +42,7 @@ import (
"github.com/apache/apisix-ingress-controller/internal/controller/config"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
"github.com/apache/apisix-ingress-controller/internal/manager/readiness"
+ "github.com/apache/apisix-ingress-controller/internal/manager/server"
"github.com/apache/apisix-ingress-controller/internal/provider"
_ "github.com/apache/apisix-ingress-controller/internal/provider/init"
_ "github.com/apache/apisix-ingress-controller/pkg/metrics"
@@ -178,6 +179,14 @@ func Run(ctx context.Context, logger logr.Logger) error {
return err
}
+ if cfg.EnableServer {
+ srv := server.NewServer(config.ControllerConfig.ServerAddr)
+ srv.Register("/debug", provider)
+ if err := mgr.Add(srv); err != nil {
+ setupLog.Error(err, "unable to add debug server to
manager")
+ return err
+ }
+ }
if err := mgr.Add(provider); err != nil {
setupLog.Error(err, "unable to add provider to manager")
return err
diff --git a/internal/manager/server/server.go
b/internal/manager/server/server.go
new file mode 100644
index 00000000..543c328b
--- /dev/null
+++ b/internal/manager/server/server.go
@@ -0,0 +1,69 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 server
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/apache/apisix-ingress-controller/internal/provider"
+)
+
+type Server struct {
+ server *http.Server
+ mux *http.ServeMux
+}
+
+func (s *Server) Start(ctx context.Context) error {
+ stop := make(chan error, 1)
+ go func() {
+ if err := s.server.ListenAndServe(); err != nil && err !=
http.ErrServerClosed {
+ stop <- err
+ }
+ close(stop)
+ }()
+ select {
+ case <-ctx.Done():
+ shutdownCtx, cancel :=
context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ return s.server.Shutdown(shutdownCtx)
+ case err := <-stop:
+ return err
+ }
+}
+
+func (s *Server) Register(pathPrefix string, registrant
provider.RegisterHandler) {
+ subMux := http.NewServeMux()
+ registrant.Register(pathPrefix, subMux)
+ s.mux.Handle(pathPrefix+"/", http.StripPrefix(pathPrefix, subMux))
+ s.mux.HandleFunc(pathPrefix, func(w http.ResponseWriter, r
*http.Request) {
+ http.Redirect(w, r, pathPrefix+"/",
http.StatusPermanentRedirect)
+ })
+}
+
+func NewServer(addr string) *Server {
+ mux := http.NewServeMux()
+ return &Server{
+ server: &http.Server{
+ Addr: addr,
+ Handler: mux,
+ },
+ mux: mux,
+ }
+}
diff --git a/internal/provider/apisix/provider.go
b/internal/provider/apisix/provider.go
index 571c5bcc..01f5f4e3 100644
--- a/internal/provider/apisix/provider.go
+++ b/internal/provider/apisix/provider.go
@@ -19,6 +19,7 @@ package apisix
import (
"context"
+ "net/http"
"sync"
"time"
@@ -89,6 +90,10 @@ func New(updater status.Updater, readier
readiness.ReadinessManager, opts ...pro
}, nil
}
+func (d *apisixProvider) Register(pathPrefix string, mux *http.ServeMux) {
+ d.client.ADCDebugProvider.SetupHandler(pathPrefix, mux)
+}
+
func (d *apisixProvider) Update(ctx context.Context, tctx
*provider.TranslateContext, obj client.Object) error {
log.Debugw("updating object", zap.Any("object", obj))
var (
@@ -231,7 +236,6 @@ func (d *apisixProvider) buildConfig(tctx
*provider.TranslateContext, nnk types.
func (d *apisixProvider) Start(ctx context.Context) error {
d.readier.WaitReady(ctx, 5*time.Minute)
-
initalSyncDelay := d.InitSyncDelay
if initalSyncDelay > 0 {
time.AfterFunc(initalSyncDelay, d.syncNotify)
diff --git a/internal/provider/common/adcdebugserver.go
b/internal/provider/common/adcdebugserver.go
new file mode 100644
index 00000000..b1ca9c5b
--- /dev/null
+++ b/internal/provider/common/adcdebugserver.go
@@ -0,0 +1,341 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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 common
+
+import (
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "net/http"
+ "net/url"
+
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+ "github.com/apache/apisix-ingress-controller/internal/adc/cache"
+ "github.com/apache/apisix-ingress-controller/internal/types"
+)
+
+type ResourceInfo struct {
+ ID string
+ Name string
+ Type string
+ Link string
+}
+
+type ADCDebugProvider struct {
+ store *cache.Store
+ configManager *ConfigManager[types.NamespacedNameKind, adctypes.Config]
+ pathPrefix string
+}
+
+func newTemplate(name, body string) *template.Template {
+ return template.Must(template.New(name).
+ Funcs(template.FuncMap{"urlencode": url.QueryEscape}).
+ Parse(body))
+}
+
+func (asrv *ADCDebugProvider) SetupHandler(pathPrefix string, mux
*http.ServeMux) {
+ asrv.pathPrefix = pathPrefix
+ mux.HandleFunc("/config", asrv.handleConfig)
+ mux.HandleFunc("/", asrv.handleIndex)
+}
+
+func NewADCDebugProvider(store *cache.Store, configManager
*ConfigManager[types.NamespacedNameKind, adctypes.Config]) *ADCDebugProvider {
+ return &ADCDebugProvider{store: store, configManager: configManager}
+}
+
+func (asrv *ADCDebugProvider) handleIndex(w http.ResponseWriter, r
*http.Request) {
+ configs := asrv.configManager.List()
+ configNames := make([]string, 0, len(configs))
+ for _, cfg := range configs {
+ configNames = append(configNames, cfg.Name)
+ }
+
+ tmpl := newTemplate("index", `
+ <html>
+ <head><title>ADC Debug Server</title></head>
+ <body>
+ <h1>Configurations</h1>
+ <ul>
+ {{range .ConfigNames}}
+ <li><a href="{{$.Prefix}}/config?name={{. |
urlencode}}">{{.}}</a></li>
+ {{end}}
+ </ul>
+ </body>
+ </html>
+ `)
+
+ _ = tmpl.Execute(w, struct {
+ ConfigNames []string
+ Prefix string
+ }{ConfigNames: configNames, Prefix: asrv.pathPrefix})
+}
+
+func (asrv *ADCDebugProvider) handleConfig(w http.ResponseWriter, r
*http.Request) {
+ configNameEncoded := r.URL.Query().Get("name")
+ if configNameEncoded == "" {
+ http.Error(w, "Config name is required", http.StatusBadRequest)
+ return
+ }
+
+ configName, err := url.QueryUnescape(configNameEncoded)
+ if err != nil {
+ http.Error(w, "Invalid config name encoding",
http.StatusBadRequest)
+ return
+ }
+
+ resourceIDEncoded := r.URL.Query().Get("id")
+ resourceID := ""
+ if resourceIDEncoded != "" {
+ resourceID, err = url.QueryUnescape(resourceIDEncoded)
+ if err != nil {
+ http.Error(w, "Invalid resource ID encoding",
http.StatusBadRequest)
+ return
+ }
+ }
+
+ resourceType := r.URL.Query().Get("type")
+
+ if resourceType == "" {
+ asrv.showResourceTypes(w, configName,
url.QueryEscape(configName))
+ return
+ }
+
+ if resourceID == "" {
+ asrv.showResources(w, r, configName,
url.QueryEscape(configName), resourceType)
+ return
+ }
+
+ asrv.showResourceDetail(w, r, configName, resourceType, resourceID)
+}
+
+func (asrv *ADCDebugProvider) showResourceTypes(w http.ResponseWriter,
configName, configNameEncoded string) {
+ resourceTypes := []string{adctypes.TypeService, adctypes.TypeRoute,
adctypes.TypeConsumer, adctypes.TypeSSL, adctypes.TypeGlobalRule,
adctypes.TypePluginMetadata}
+
+ tmpl := newTemplate("resources", `
+ <html>
+ <head><title>Resources for {{.ConfigName}}</title></head>
+ <body>
+ <h1>Resources for {{.ConfigName}}</h1>
+ <ul>
+ {{range .ResourceTypes}}
+ <li><a
href="{{$.Prefix}}/config?name={{$.ConfigNameEncoded}}&type={{. |
urlencode}}">{{.}}</a></li>
+ {{end}}
+ </ul>
+ </body>
+ </html>
+ `)
+
+ _ = tmpl.Execute(w, struct {
+ ConfigName string
+ ConfigNameEncoded string
+ ResourceTypes []string
+ Prefix string
+ }{
+ ConfigName: configName,
+ ConfigNameEncoded: configNameEncoded,
+ ResourceTypes: resourceTypes,
+ Prefix: asrv.pathPrefix,
+ })
+}
+
+func (asrv *ADCDebugProvider) showResources(w http.ResponseWriter, r
*http.Request, configName, configNameEncoded, resourceType string) {
+ resources, err := asrv.store.GetResources(configName)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ var resourceInfos []ResourceInfo
+ switch resourceType {
+ case adctypes.TypeService:
+ for _, svc := range resources.Services {
+ resourceInfos = append(resourceInfos, ResourceInfo{
+ ID: svc.ID,
+ Name: svc.Name,
+ Type: resourceType,
+ Link:
fmt.Sprintf("%s/config?name=%s&type=%s&id=%s",
+ asrv.pathPrefix, configNameEncoded,
url.QueryEscape(resourceType), url.QueryEscape(svc.ID)),
+ })
+ }
+ case adctypes.TypeConsumer:
+ for _, consumer := range resources.Consumers {
+ resourceInfos = append(resourceInfos, ResourceInfo{
+ ID: consumer.Username,
+ Name: consumer.Username,
+ Type: resourceType,
+ Link:
fmt.Sprintf("%s/config?name=%s&type=%s&id=%s",
+ asrv.pathPrefix, configNameEncoded,
url.QueryEscape(resourceType), url.QueryEscape(consumer.Username)),
+ })
+ }
+ case adctypes.TypeSSL:
+ for _, ssl := range resources.SSLs {
+ resourceInfos = append(resourceInfos, ResourceInfo{
+ ID: ssl.ID,
+ Name: ssl.ID,
+ Type: resourceType,
+ Link:
fmt.Sprintf("%s/config?name=%s&type=%s&id=%s",
+ asrv.pathPrefix, configNameEncoded,
url.QueryEscape(resourceType), url.QueryEscape(ssl.ID)),
+ })
+ }
+ case adctypes.TypeGlobalRule:
+ for key := range resources.GlobalRules {
+ resourceInfos = append(resourceInfos, ResourceInfo{
+ ID: key,
+ Name: key,
+ Type: resourceType,
+ Link:
fmt.Sprintf("%s/config?name=%s&type=%s&id=%s",
+ asrv.pathPrefix, configNameEncoded,
url.QueryEscape(resourceType), url.QueryEscape(key)),
+ })
+ }
+ case adctypes.TypePluginMetadata:
+ if resources.PluginMetadata != nil {
+ resourceInfos = append(resourceInfos, ResourceInfo{
+ ID: "pluginmetadata",
+ Name: "Plugin Metadata",
+ Type: resourceType,
+ Link:
fmt.Sprintf("%s/config?name=%s&type=%s&id=%s",
+ asrv.pathPrefix, configNameEncoded,
url.QueryEscape(resourceType), "pluginmetadata"),
+ })
+ }
+ case adctypes.TypeRoute:
+ for _, svc := range resources.Services {
+ for _, route := range svc.Routes {
+ resourceInfos = append(resourceInfos,
ResourceInfo{
+ ID: route.ID,
+ Name: route.Name,
+ Type: resourceType,
+ Link:
fmt.Sprintf("%s/config?name=%s&type=%s&id=%s",
+ asrv.pathPrefix,
configNameEncoded, url.QueryEscape(resourceType), url.QueryEscape(route.ID)),
+ })
+ }
+ }
+ default:
+ http.NotFound(w, r)
+ return
+ }
+
+ tmpl := newTemplate("resourceList", `
+ <html>
+ <head><title>{{.ResourceType}} for
{{.ConfigName}}</title></head>
+ <body>
+ <h1>{{.ResourceType}} for {{.ConfigName}}</h1>
+ <ul>
+ {{range .Resources}}
+ <li><a href="{{.Link}}">{{.Name}}
({{.ID}})</a></li>
+ {{end}}
+ </ul>
+ </body>
+ </html>
+ `)
+
+ _ = tmpl.Execute(w, struct {
+ ConfigName string
+ ResourceType string
+ Resources []ResourceInfo
+ Prefix string
+ }{
+ ConfigName: configName,
+ ResourceType: resourceType,
+ Resources: resourceInfos,
+ Prefix: asrv.pathPrefix,
+ })
+}
+
+func (asrv *ADCDebugProvider) showResourceDetail(w http.ResponseWriter, r
*http.Request, configName, resourceType, resourceID string) {
+ resources, err := asrv.store.GetResources(configName)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ var resource interface{}
+ switch resourceType {
+ case adctypes.TypeService:
+ for _, svc := range resources.Services {
+ if svc.ID == resourceID {
+ resource = svc
+ break
+ }
+ }
+ case adctypes.TypeConsumer:
+ for _, consumer := range resources.Consumers {
+ if consumer.Username == resourceID {
+ resource = consumer
+ break
+ }
+ }
+ case adctypes.TypeSSL:
+ for _, ssl := range resources.SSLs {
+ if ssl.ID == resourceID {
+ resource = ssl
+ break
+ }
+ }
+ case adctypes.TypeGlobalRule:
+ resource = resources.GlobalRules
+ case adctypes.TypePluginMetadata:
+ resource = resources.PluginMetadata
+ case adctypes.TypeRoute:
+ for _, svc := range resources.Services {
+ for _, route := range svc.Routes {
+ if route.ID == resourceID {
+ resource = route
+ break
+ }
+ }
+ }
+ default:
+ http.NotFound(w, r)
+ return
+ }
+
+ if resource == nil {
+ http.NotFound(w, r)
+ return
+ }
+
+ jsonData, err := json.MarshalIndent(resource, "", " ")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ tmpl := newTemplate("resourceDetail", `
+ <html>
+ <head><title>Resource Detail</title></head>
+ <body>
+ <h1>Resource Details:
{{.ResourceType}}/{{.ResourceID}}</h1>
+ <pre>{{.Resource}}</pre>
+ <a href="{{.Prefix}}/config?name={{.ConfigName |
urlencode}}&type={{.ResourceType}}">Back</a>
+ </body>
+ </html>
+ `)
+
+ _ = tmpl.Execute(w, struct {
+ ConfigName string
+ Resource string
+ ResourceID string
+ ResourceType string
+ Prefix string
+ }{
+ ConfigName: configName,
+ Resource: string(jsonData),
+ ResourceID: resourceID,
+ ResourceType: resourceType,
+ Prefix: asrv.pathPrefix,
+ })
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index ef93de54..3f0bcded 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -33,6 +33,7 @@ import (
)
type Provider interface {
+ RegisterHandler
Update(context.Context, *TranslateContext, client.Object) error
Delete(context.Context, client.Object) error
Start(context.Context) error
diff --git a/internal/provider/register.go b/internal/provider/register.go
index a2542ad7..25cc670d 100644
--- a/internal/provider/register.go
+++ b/internal/provider/register.go
@@ -19,11 +19,16 @@ package provider
import (
"fmt"
+ "net/http"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
"github.com/apache/apisix-ingress-controller/internal/manager/readiness"
)
+type RegisterHandler interface {
+ Register(pathPrefix string, mux *http.ServeMux)
+}
+
type RegisterFunc func(status.Updater, readiness.ReadinessManager, ...Option)
(Provider, error)
var providers = map[string]RegisterFunc{}