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{}

Reply via email to