Skip to content
Merged
2 changes: 2 additions & 0 deletions config/samples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 9 additions & 6 deletions internal/adc/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,24 @@ 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) {
serverURL := os.Getenv("ADC_SERVER_URL")
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
}

Expand Down
1 change: 1 addition & 0 deletions internal/controller/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
3 changes: 3 additions & 0 deletions internal/controller/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const (

DefaultMetricsAddr = ":8080"
DefaultProbeAddr = ":8081"
DefaultServerAddr = ":9092"
)

// Config contains all config items which are necessary for
Expand All @@ -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"`
Expand Down
9 changes: 9 additions & 0 deletions internal/manager/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
69 changes: 69 additions & 0 deletions internal/manager/server/server.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
Comment on lines +51 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug server currently has no authentication mechanism, making it accessible to anyone who can reach the endpoint. Should we consider incorporating certain authentication mechanisms?

Copy link
Contributor Author

@Revolyssup Revolyssup Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original requirement was to return simple UI friendly HTML. Adding authentication will mean adding another page for login. Maybe we can use cookie based authentication and add one more simple login html page.

  1. Check if cookie has INGRESS-DEBUG_TOKEN. If present then serve normally.
  2. If not present then redirect to login page. This will also be a simple HTML page with just input box to enter debug token. This debug token will be set in the static configuration for ingress and can be copied from there and pasted here. User clicks login and this value be just stored in the cookie if matches and user will be logged in.

@bzp2010 @ronething @AlinsRan What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's enough not to expose the port, just like the control-api.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's enough not to expose the port, just like the control-api.

It might be a bit different. This debug server has exposed SSL/Consumer resources, but the control-api does not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be a bit different. This debug server has exposed SSL/Consumer resources, but the control-api does not.

You're right.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the only k8s native way to enable/disable this dynamically will be to use some existing CR for it. But that might be an overkill. I think we should opt for one of the two ways:

  1. Just disable this server by default and have it enabled statically only and not dynamically. It's a debug server, I don't know if dynamically enabling/disabling it is good enough use case. This is the simplest and safest.
  2. Just have a single login page that expects a statically defined key as recommended above.

If it's okay to enable/disable this statically then I recommend the first way is good enough or else it will be overengineering. Later if use case emerges about dynamically enabling/disabling, we can spend some thought here.

Copy link
Contributor

@bzp2010 bzp2010 Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's exactly what I was thinking. A simple static config.

However, I must confirm whether the restart required to enable this feature would disrupt the current "state". Specifically, can we guarantee consistent results based on unchanged configurations? We need to ensure that the restart does not corrupt any potentially existing in-memory state, thereby preventing the debugging of previously existing issues.

For example, we sometimes encounter problems that resolve after a restart, making it impossible to analyze the root cause.

Can anyone answer this question?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not possible to guarantee the preservation of “state” across a restart.
The controller’s declarative configuration state (stored in Kubernetes/etcd via CRDs) is automatically rebuilt after a restart, but any runtime in-memory state (caches, queues, goroutine stacks, etc.) will inevitably be lost.

For an ingress controller, a restart is typically the last-resort mechanism to recover state, so if you need to debug live in-memory data, it will naturally no longer be available after restarting.

At present, for security and simplicity reasons, we recommend keeping the debug API disabled by default and enabling it only via static configuration.
If there is a strong need to inspect runtime state in the future, we can revisit supporting dynamic enablement or enabling it up front with proper authentication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. added the boolean in static config.

Copy link
Contributor

@bzp2010 bzp2010 Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state itself isn't critical, but it would be preferable if you could confirm it doesn't impact configuration generation.

The key point is For a given input of Kubernetes resources, always produce a fixed output. This includes how to handle conflicting resources (determine the priority), how to sort resource lists, and so on. This process already exists within AIC, such as during full synchronization at startup. We can ignore those changes triggered by reconciliation tasks.
If these could be leveraged for debugging functionality, that would be excellent.


func NewServer(addr string) *Server {
mux := http.NewServeMux()
return &Server{
server: &http.Server{
Addr: addr,
Handler: mux,
},
mux: mux,
}
}
6 changes: 5 additions & 1 deletion internal/provider/apisix/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package apisix

import (
"context"
"net/http"
"sync"
"time"

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -228,7 +233,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)
Expand Down
Loading
Loading