diff --git a/config/samples/config.yaml b/config/samples/config.yaml index 6656cb90f..4d3c76a80 100644 --- a/config/samples/config.yaml +++ b/config/samples/config.yaml @@ -19,6 +19,8 @@ leader_election: metrics_addr: ":8080" # The address the metrics endpoint binds to. # The default value is ":8080". +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/docs/en/latest/reference/configuration-file.md b/docs/en/latest/reference/configuration-file.md index dc3d82c7f..588d945ae 100644 --- a/docs/en/latest/reference/configuration-file.md +++ b/docs/en/latest/reference/configuration-file.md @@ -51,6 +51,8 @@ leader_election: metrics_addr: ":8080" # The address the metrics endpoint binds to. # The default value is ":8080". +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 9bf38321e..e257c9531 100644 --- a/internal/adc/client/client.go +++ b/internal/adc/client/client.go @@ -46,7 +46,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) { @@ -55,12 +56,15 @@ func New(mode string, timeout time.Duration) (*Client, error) { 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 ea8dfbac3..2b1140041 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 8d167a2be..843f1a8df 100644 --- a/internal/controller/config/types.go +++ b/internal/controller/config/types.go @@ -60,6 +60,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 12a074ea4..de64f62a2 100644 --- a/internal/manager/run.go +++ b/internal/manager/run.go @@ -43,6 +43,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" @@ -189,6 +190,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 000000000..543c328bd --- /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/api7ee/provider.go b/internal/provider/api7ee/provider.go index 3c3784a00..4ce4af064 100644 --- a/internal/provider/api7ee/provider.go +++ b/internal/provider/api7ee/provider.go @@ -19,6 +19,7 @@ package api7ee import ( "context" + "net/http" "sync/atomic" "time" @@ -84,6 +85,10 @@ func New(updater status.Updater, readier readiness.ReadinessManager, opts ...pro }, nil } +func (d *api7eeProvider) Register(pathPrefix string, mux *http.ServeMux) { + d.client.ADCDebugProvider.SetupHandler(pathPrefix, mux) +} + func (d *api7eeProvider) Update(ctx context.Context, tctx *provider.TranslateContext, obj client.Object) error { log.Debugw("updating object", zap.Any("object", obj)) var ( diff --git a/internal/provider/apisix/provider.go b/internal/provider/apisix/provider.go index 395ba62c7..846923305 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" @@ -91,6 +92,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 ( diff --git a/internal/provider/common/adcdebugserver.go b/internal/provider/common/adcdebugserver.go new file mode 100644 index 000000000..b1ca9c5b2 --- /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", ` + + ADC Debug Server + +

Configurations

+ + + + `) + + _ = 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", ` + + Resources for {{.ConfigName}} + +

Resources for {{.ConfigName}}

+ + + + `) + + _ = 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", ` + + {{.ResourceType}} for {{.ConfigName}} + +

{{.ResourceType}} for {{.ConfigName}}

+ + + + `) + + _ = 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", ` + + Resource Detail + +

Resource Details: {{.ResourceType}}/{{.ResourceID}}

+
{{.Resource}}
+ Back + + + `) + + _ = 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 ef93de541..3f0bcded4 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 a2542ad70..25cc670dc 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{}