diff --git a/README.md b/README.md index 36977d060..de25a16d4 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ server: # host of the control plane host: 127.0.0.1 grpcPort: 54789 # control plane grpc port +api: + port: 9090 # nginx-agent http api # tls options - NOT RECOMMENDED FOR PRODUCTION tls: enable: false diff --git a/main.go b/main.go index 96b51134a..d7d9d7ae0 100644 --- a/main.go +++ b/main.go @@ -189,6 +189,7 @@ func loadPlugins(commander client.Commander, binary *core.NginxBinaryType, env * plugins.NewFileWatcher(loadedConfig, env), plugins.NewFileWatchThrottle(), plugins.NewEvents(loadedConfig, env, sdkGRPC.NewMessageMeta(uuid.NewString()), binary), + plugins.NewAgentAPI(loadedConfig, env, binary), ) if len(loadedConfig.Nginx.NginxCountingSocket) > 0 { diff --git a/nginx-agent.conf b/nginx-agent.conf index 549de3b38..ebd24b4e9 100644 --- a/nginx-agent.conf +++ b/nginx-agent.conf @@ -15,6 +15,9 @@ server: # provide servername overrides if using SNI # metrics: "" # command: "" +api: + # port to expose http api + port: 9090 # tls options tls: # enable tls in the nginx-agent setup for grpcs diff --git a/src/core/config/config.go b/src/core/config/config.go index bd3a65246..63e74ce4d 100644 --- a/src/core/config/config.go +++ b/src/core/config/config.go @@ -162,6 +162,7 @@ func GetConfig(clientId string) (*Config, error) { ClientID: clientId, CloudAccountID: Viper.GetString(CloudAccountIdKey), Server: getServer(), + AgentAPI: getAgentAPI(), ConfigDirs: Viper.GetString(ConfigDirsKey), Log: getLog(), TLS: getTLS(), @@ -326,13 +327,19 @@ func getNginx() Nginx { func getServer() Server { return Server{ Host: Viper.GetString(ServerHost), - GrpcPort: Viper.GetInt(ServerGrpcport), + GrpcPort: Viper.GetInt(ServerGrpcPort), Token: Viper.GetString(ServerToken), Metrics: Viper.GetString(ServerMetrics), Command: Viper.GetString(ServerCommand), } } +func getAgentAPI() AgentAPI { + return AgentAPI{ + Port: Viper.GetInt(AgentAPIPort), + } +} + func getTLS() TLSConfig { return TLSConfig{ Enable: Viper.GetBool(TlsEnable), diff --git a/src/core/config/config_test.go b/src/core/config/config_test.go index 7af6da135..aa30bab54 100644 --- a/src/core/config/config_test.go +++ b/src/core/config/config_test.go @@ -13,13 +13,13 @@ import ( "github.com/stretchr/testify/require" agent_config "github.com/nginx/agent/sdk/v2/agent/config" - sysutils "github.com/nginx/agent/v2/test/utils/system" ) const ( updatedServerHost = "192.168.0.1" - updatedServerPort = 11000 + updatedServerGrpcPort = 11000 + updatedAgentAPIPort = 9010 updatedLogLevel = "fatal" updatedLogPath = "./test-path" updatedConfigDirs = "/usr/local/etc/nginx" @@ -139,6 +139,9 @@ func TestGetConfig(t *testing.T) { assert.Equal(t, Defaults.Server.GrpcPort, config.Server.GrpcPort) assert.Equal(t, Defaults.Server.Command, config.Server.Command) assert.Equal(t, Defaults.Server.Metrics, config.Server.Metrics) + + assert.Equal(t, Defaults.AgentAPI.Port, config.AgentAPI.Port) + assert.True(t, len(config.AllowedDirectoriesMap) > 0) assert.Equal(t, Defaults.ConfigDirs, config.ConfigDirs) assert.Equal(t, Defaults.TLS.Enable, config.TLS.Enable) @@ -189,11 +192,12 @@ func TestGetConfig(t *testing.T) { // Check for updated values assert.Equal(t, updatedLogLevel, config.Log.Level) assert.Equal(t, updatedTag, config.DisplayName) + assert.Equal(t, []string{updatedTag}, config.Tags) // Everything else should still be default assert.Equal(t, Defaults.Server.Host, config.Server.Host) assert.Equal(t, Defaults.Server.GrpcPort, config.Server.GrpcPort) - assert.Equal(t, []string{updatedTag}, config.Tags) + assert.Equal(t, Defaults.AgentAPI.Port, config.AgentAPI.Port) }) t.Run("test override defaults with config file values", func(t *testing.T) { @@ -249,7 +253,8 @@ func TestGetConfig(t *testing.T) { require.NoError(t, err) assert.Equal(t, updatedServerHost, config.Server.Host) - assert.Equal(t, updatedServerPort, config.Server.GrpcPort) + assert.Equal(t, updatedServerGrpcPort, config.Server.GrpcPort) + assert.Equal(t, updatedAgentAPIPort, config.AgentAPI.Port) assert.Equal(t, updatedConfTags, config.Tags) // Check for updated values diff --git a/src/core/config/defaults.go b/src/core/config/defaults.go index 2cdfc9140..25c599879 100644 --- a/src/core/config/defaults.go +++ b/src/core/config/defaults.go @@ -4,9 +4,9 @@ import ( "os" "time" - "github.com/google/uuid" - agent_config "github.com/nginx/agent/sdk/v2/agent/config" + + "github.com/google/uuid" log "github.com/sirupsen/logrus" ) @@ -45,6 +45,9 @@ var ( // so setting to random uuid at the moment, tls connection won't work without the auth header Token: uuid.New().String(), }, + AgentAPI: AgentAPI{ + Port: 9090, + }, Nginx: Nginx{ Debug: false, NginxCountingSocket: "unix:/var/run/nginx-agent/nginx.sock", @@ -107,11 +110,16 @@ const ( ServerKey = "server" ServerHost = ServerKey + agent_config.KeyDelimiter + "host" - ServerGrpcport = ServerKey + agent_config.KeyDelimiter + "grpcport" + ServerGrpcPort = ServerKey + agent_config.KeyDelimiter + "grpcport" ServerToken = ServerKey + agent_config.KeyDelimiter + "token" ServerMetrics = ServerKey + agent_config.KeyDelimiter + "metrics" ServerCommand = ServerKey + agent_config.KeyDelimiter + "command" + // viper keys used in config + APIKey = "api" + + AgentAPIPort = APIKey + agent_config.KeyDelimiter + "port" + // viper keys used in config TlsKey = "tls" @@ -203,7 +211,7 @@ var ( DefaultValue: Defaults.Server.Host, }, &IntFlag{ - Name: ServerGrpcport, + Name: ServerGrpcPort, Usage: "The desired GRPC port to use for nginx-agent traffic.", DefaultValue: Defaults.Server.GrpcPort, }, @@ -222,6 +230,12 @@ var ( Usage: "The name of the command server sent in the tls configuration.", DefaultValue: Defaults.Server.Command, }, + // API Config + &IntFlag{ + Name: AgentAPIPort, + Usage: "The desired port to use for nginx-agent to expose for HTTP traffic.", + DefaultValue: Defaults.AgentAPI.Port, + }, &StringFlag{ Name: ConfigDirsKey, Usage: "Defines the paths that you want to grant nginx-agent read/write access to. This key is formatted as a string and follows Unix PATH format.", diff --git a/src/core/config/types.go b/src/core/config/types.go index f932e4b22..976cee3a6 100644 --- a/src/core/config/types.go +++ b/src/core/config/types.go @@ -12,6 +12,7 @@ type Config struct { ClientID string `mapstructure:"agent_id" yaml:"-"` CloudAccountID string `mapstructure:"cloud_account" yaml:"-"` Server Server `mapstructure:"server" yaml:"-"` + AgentAPI AgentAPI `mapstructure:"api" yaml:"-"` ConfigDirs string `mapstructure:"config-dirs" yaml:"-"` Log LogConfig `mapstructure:"log" yaml:"-"` TLS TLSConfig `mapstructure:"tls" yaml:"-"` @@ -40,6 +41,10 @@ type Server struct { Target string `mapstructure:"target" yaml:"-"` } +type AgentAPI struct { + Port int `mapstructure:"port" yaml:"-"` +} + // LogConfig for logging type LogConfig struct { Level string `mapstructure:"level" yaml:"-"` diff --git a/src/plugins/agent_api.go b/src/plugins/agent_api.go new file mode 100644 index 000000000..5bb369be2 --- /dev/null +++ b/src/plugins/agent_api.go @@ -0,0 +1,134 @@ +package plugins + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "regexp" + + "github.com/nginx/agent/sdk/v2/proto" + "github.com/nginx/agent/v2/src/core" + "github.com/nginx/agent/v2/src/core/config" + + log "github.com/sirupsen/logrus" +) + +type AgentAPI struct { + config *config.Config + env core.Environment + server http.Server + nginxBinary core.NginxBinary + nginxHandler *NginxHandler +} + +type NginxHandler struct { + env core.Environment + nginxBinary core.NginxBinary +} + +func NewAgentAPI(config *config.Config, env core.Environment, nginxBinary core.NginxBinary) *AgentAPI { + return &AgentAPI{config: config, env: env, nginxBinary: nginxBinary} +} + +func (a *AgentAPI) Init(core.MessagePipeInterface) { + log.Info("Agent API initializing") + go a.createHttpServer() +} + +func (a *AgentAPI) Close() { + log.Info("Agent API is wrapping up") + if err := a.server.Shutdown(context.Background()); err != nil { + log.Errorf("Agent API HTTP Server Shutdown Error: %v", err) + } +} + +func (a *AgentAPI) Process(message *core.Message) { + log.Tracef("Process function in the agent_api.go, %s %v", message.Topic(), message.Data()) +} + +func (a *AgentAPI) Info() *core.Info { + return core.NewInfo("Agent API Plugin", "v0.0.1") +} + +func (a *AgentAPI) Subscriptions() []string { + return []string{} +} + +func (a *AgentAPI) createHttpServer() { + mux := http.NewServeMux() + a.nginxHandler = &NginxHandler{a.env, a.nginxBinary} + mux.Handle("/nginx/", a.nginxHandler) + + log.Debug("Starting Agent API HTTP server") + + a.server = http.Server{ + Addr: fmt.Sprintf(":%d", a.config.AgentAPI.Port), + Handler: mux, + } + + if err := a.server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("error listening to port: %v", err) + } +} + +var ( + instancesRegex = regexp.MustCompile(`^\/nginx[\/]*$`) +) + +func (h *NginxHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + switch { + case r.Method == http.MethodGet && instancesRegex.MatchString(r.URL.Path): + err := sendInstanceDetailsPayload(h.getNginxDetails(), w, r) + if err != nil { + log.Warnf("Failed to send instance details payload: %v", err) + } + default: + w.WriteHeader(http.StatusNotFound) + _, err := fmt.Fprint(w, []byte("not found")) + if err != nil { + log.Warnf("Failed to send api response: %v", err) + } + } +} + +func sendInstanceDetailsPayload(nginxDetails []*proto.NginxDetails, w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(http.StatusOK) + + if len(nginxDetails) == 0 { + log.Debug("No nginx instances found") + _, err := fmt.Fprint(w, "[]") + if err != nil { + return fmt.Errorf("failed to send payload: %v", err) + } + + return nil + } + + respBody := new(bytes.Buffer) + err := json.NewEncoder(respBody).Encode(nginxDetails) + if err != nil { + return fmt.Errorf("failed to encode payload: %v", err) + } + + _, err = fmt.Fprint(w, respBody) + if err != nil { + return fmt.Errorf("failed to send payload: %v", err) + } + + return nil +} + +func (h *NginxHandler) getNginxDetails() []*proto.NginxDetails { + var nginxDetails []*proto.NginxDetails + + for _, proc := range h.env.Processes() { + if proc.IsMaster { + nginxDetails = append(nginxDetails, h.nginxBinary.GetNginxDetailsFromProcess(proc)) + } + } + + return nginxDetails +} diff --git a/src/plugins/agent_api_test.go b/src/plugins/agent_api_test.go new file mode 100644 index 000000000..2b243167f --- /dev/null +++ b/src/plugins/agent_api_test.go @@ -0,0 +1,78 @@ +package plugins + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/nginx/agent/sdk/v2/proto" + "github.com/stretchr/testify/assert" +) + +func TestNginxHandler_sendInstanceDetailsPayload(t *testing.T) { + tests := []struct { + name string + nginxDetails []*proto.NginxDetails + }{ + { + name: "no instances", + nginxDetails: []*proto.NginxDetails{}, + }, + { + name: "single instance", + nginxDetails: []*proto.NginxDetails{ + { + NginxId: "1", Version: "21", ConfPath: "/etc/yo", ProcessId: "123", StartTime: 1238043824, + BuiltFromSource: false, + LoadableModules: []string{}, + RuntimeModules: []string{}, + Plus: &proto.NginxPlusMetaData{Enabled: true}, + ConfigureArgs: []string{}, + }, + }, + }, + { + name: "multi instance", + nginxDetails: []*proto.NginxDetails{ + { + NginxId: "1", Version: "21", ConfPath: "/etc/yo", ProcessId: "123", StartTime: 1238043824, + BuiltFromSource: false, + LoadableModules: []string{}, + RuntimeModules: []string{}, + Plus: &proto.NginxPlusMetaData{Enabled: true}, + ConfigureArgs: []string{}, + }, + { + NginxId: "2", Version: "21", ConfPath: "/etc/yo", ProcessId: "123", StartTime: 1238043824, + BuiltFromSource: false, + LoadableModules: []string{}, + RuntimeModules: []string{}, + Plus: &proto.NginxPlusMetaData{Enabled: true}, + ConfigureArgs: []string{}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + respRec := httptest.NewRecorder() + path := "/nginx/" + req := httptest.NewRequest(http.MethodGet, path, nil) + + err := sendInstanceDetailsPayload(tt.nginxDetails, respRec, req) + assert.NoError(t, err) + + resp := respRec.Result() + defer resp.Body.Close() + + var nginxDetailsResponse []*proto.NginxDetails + err = json.Unmarshal(respRec.Body.Bytes(), &nginxDetailsResponse) + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.True(t, json.Valid(respRec.Body.Bytes())) + assert.Equal(t, tt.nginxDetails, nginxDetailsResponse) + }) + } +} diff --git a/src/plugins/testdata/configs/nginx-agent.conf b/src/plugins/testdata/configs/nginx-agent.conf index f3e21a7ef..a759d44df 100644 --- a/src/plugins/testdata/configs/nginx-agent.conf +++ b/src/plugins/testdata/configs/nginx-agent.conf @@ -10,6 +10,8 @@ server: token: "goodfellow" command: "test-server-command" metrics: "test-server-metrics" +api: + port: 9090 # tls options tls: # enable tls in the nginx-agent setup for grpcs diff --git a/src/plugins/testdata/configs/updated.conf b/src/plugins/testdata/configs/updated.conf index 32874c4fe..fe171db67 100644 --- a/src/plugins/testdata/configs/updated.conf +++ b/src/plugins/testdata/configs/updated.conf @@ -1,6 +1,8 @@ server: host: 192.168.0.1 grpcPort: 11000 +api: + port: 9010 config_dirs: /usr/local/etc/nginx log: level: fatal diff --git a/test/component/agent_api_test.go b/test/component/agent_api_test.go new file mode 100644 index 000000000..c68a80774 --- /dev/null +++ b/test/component/agent_api_test.go @@ -0,0 +1,108 @@ +package component + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/nginx/agent/sdk/v2/proto" + "github.com/nginx/agent/v2/src/core" + "github.com/nginx/agent/v2/src/core/config" + "github.com/nginx/agent/v2/src/plugins" + tutils "github.com/nginx/agent/v2/test/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetNginxInstances(t *testing.T) { + port := 9090 + processID := "12345" + var pid int32 = 12345 + + tests := []struct { + name string + nginxDetails *proto.NginxDetails + expectedJSON string + }{ + { + name: "no instances", + nginxDetails: nil, + }, + { + name: "single instance", + nginxDetails: &proto.NginxDetails{ + NginxId: "45d4sf5d4sf4e8s4f8es4564", Version: "21", ConfPath: "/etc/nginx/conf", ProcessId: processID, StartTime: 1238043824, + BuiltFromSource: false, + Plus: &proto.NginxPlusMetaData{Enabled: true}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := &config.Config{ + AgentAPI: config.AgentAPI{ + Port: port, + }, + } + + mockEnvironment := tutils.NewMockEnvironment() + if tt.nginxDetails == nil { + mockEnvironment.On("Processes").Return([]core.Process{{Pid: pid, IsMaster: false}}) + } else { + mockEnvironment.On("Processes").Return([]core.Process{{Pid: pid, IsMaster: true}}) + } + + mockNginxBinary := tutils.NewMockNginxBinary() + mockNginxBinary.On("GetNginxDetailsFromProcess", mock.Anything).Return(tt.nginxDetails) + + agentAPI := plugins.NewAgentAPI(conf, mockEnvironment, mockNginxBinary) + agentAPI.Init(core.NewMockMessagePipe(context.TODO())) + + response, err := http.Get(fmt.Sprintf("http://localhost:%d/nginx/", port)) + assert.Nil(t, err) + + responseData, err := io.ReadAll(response.Body) + assert.Nil(t, err) + + var nginxDetailsResponse []*proto.NginxDetails + err = json.Unmarshal(responseData, &nginxDetailsResponse) + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.StatusCode) + assert.True(t, json.Valid(responseData)) + if tt.nginxDetails == nil { + assert.Equal(t, 0, len(nginxDetailsResponse)) + } else { + assert.Equal(t, 1, len(nginxDetailsResponse)) + assert.Equal(t, tt.nginxDetails, nginxDetailsResponse[0]) + } + + agentAPI.Close() + }) + } +} + +func TestInvalidPath(t *testing.T) { + port := 9090 + + conf := &config.Config{ + AgentAPI: config.AgentAPI{ + Port: port, + }, + } + + mockEnvironment := tutils.NewMockEnvironment() + mockNginxBinary := tutils.NewMockNginxBinary() + + agentAPI := plugins.NewAgentAPI(conf, mockEnvironment, mockNginxBinary) + agentAPI.Init(core.NewMockMessagePipe(context.TODO())) + + response, err := http.Get(fmt.Sprintf("http://localhost:%d/invalid/", port)) + assert.Nil(t, err) + + assert.Equal(t, http.StatusNotFound, response.StatusCode) +} diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go index bd3a65246..63e74ce4d 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/config.go @@ -162,6 +162,7 @@ func GetConfig(clientId string) (*Config, error) { ClientID: clientId, CloudAccountID: Viper.GetString(CloudAccountIdKey), Server: getServer(), + AgentAPI: getAgentAPI(), ConfigDirs: Viper.GetString(ConfigDirsKey), Log: getLog(), TLS: getTLS(), @@ -326,13 +327,19 @@ func getNginx() Nginx { func getServer() Server { return Server{ Host: Viper.GetString(ServerHost), - GrpcPort: Viper.GetInt(ServerGrpcport), + GrpcPort: Viper.GetInt(ServerGrpcPort), Token: Viper.GetString(ServerToken), Metrics: Viper.GetString(ServerMetrics), Command: Viper.GetString(ServerCommand), } } +func getAgentAPI() AgentAPI { + return AgentAPI{ + Port: Viper.GetInt(AgentAPIPort), + } +} + func getTLS() TLSConfig { return TLSConfig{ Enable: Viper.GetBool(TlsEnable), diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go index 2cdfc9140..25c599879 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/defaults.go @@ -4,9 +4,9 @@ import ( "os" "time" - "github.com/google/uuid" - agent_config "github.com/nginx/agent/sdk/v2/agent/config" + + "github.com/google/uuid" log "github.com/sirupsen/logrus" ) @@ -45,6 +45,9 @@ var ( // so setting to random uuid at the moment, tls connection won't work without the auth header Token: uuid.New().String(), }, + AgentAPI: AgentAPI{ + Port: 9090, + }, Nginx: Nginx{ Debug: false, NginxCountingSocket: "unix:/var/run/nginx-agent/nginx.sock", @@ -107,11 +110,16 @@ const ( ServerKey = "server" ServerHost = ServerKey + agent_config.KeyDelimiter + "host" - ServerGrpcport = ServerKey + agent_config.KeyDelimiter + "grpcport" + ServerGrpcPort = ServerKey + agent_config.KeyDelimiter + "grpcport" ServerToken = ServerKey + agent_config.KeyDelimiter + "token" ServerMetrics = ServerKey + agent_config.KeyDelimiter + "metrics" ServerCommand = ServerKey + agent_config.KeyDelimiter + "command" + // viper keys used in config + APIKey = "api" + + AgentAPIPort = APIKey + agent_config.KeyDelimiter + "port" + // viper keys used in config TlsKey = "tls" @@ -203,7 +211,7 @@ var ( DefaultValue: Defaults.Server.Host, }, &IntFlag{ - Name: ServerGrpcport, + Name: ServerGrpcPort, Usage: "The desired GRPC port to use for nginx-agent traffic.", DefaultValue: Defaults.Server.GrpcPort, }, @@ -222,6 +230,12 @@ var ( Usage: "The name of the command server sent in the tls configuration.", DefaultValue: Defaults.Server.Command, }, + // API Config + &IntFlag{ + Name: AgentAPIPort, + Usage: "The desired port to use for nginx-agent to expose for HTTP traffic.", + DefaultValue: Defaults.AgentAPI.Port, + }, &StringFlag{ Name: ConfigDirsKey, Usage: "Defines the paths that you want to grant nginx-agent read/write access to. This key is formatted as a string and follows Unix PATH format.", diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go index f932e4b22..976cee3a6 100644 --- a/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/core/config/types.go @@ -12,6 +12,7 @@ type Config struct { ClientID string `mapstructure:"agent_id" yaml:"-"` CloudAccountID string `mapstructure:"cloud_account" yaml:"-"` Server Server `mapstructure:"server" yaml:"-"` + AgentAPI AgentAPI `mapstructure:"api" yaml:"-"` ConfigDirs string `mapstructure:"config-dirs" yaml:"-"` Log LogConfig `mapstructure:"log" yaml:"-"` TLS TLSConfig `mapstructure:"tls" yaml:"-"` @@ -40,6 +41,10 @@ type Server struct { Target string `mapstructure:"target" yaml:"-"` } +type AgentAPI struct { + Port int `mapstructure:"port" yaml:"-"` +} + // LogConfig for logging type LogConfig struct { Level string `mapstructure:"level" yaml:"-"` diff --git a/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/agent_api.go b/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/agent_api.go new file mode 100644 index 000000000..5bb369be2 --- /dev/null +++ b/test/performance/vendor/github.com/nginx/agent/v2/src/plugins/agent_api.go @@ -0,0 +1,134 @@ +package plugins + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "regexp" + + "github.com/nginx/agent/sdk/v2/proto" + "github.com/nginx/agent/v2/src/core" + "github.com/nginx/agent/v2/src/core/config" + + log "github.com/sirupsen/logrus" +) + +type AgentAPI struct { + config *config.Config + env core.Environment + server http.Server + nginxBinary core.NginxBinary + nginxHandler *NginxHandler +} + +type NginxHandler struct { + env core.Environment + nginxBinary core.NginxBinary +} + +func NewAgentAPI(config *config.Config, env core.Environment, nginxBinary core.NginxBinary) *AgentAPI { + return &AgentAPI{config: config, env: env, nginxBinary: nginxBinary} +} + +func (a *AgentAPI) Init(core.MessagePipeInterface) { + log.Info("Agent API initializing") + go a.createHttpServer() +} + +func (a *AgentAPI) Close() { + log.Info("Agent API is wrapping up") + if err := a.server.Shutdown(context.Background()); err != nil { + log.Errorf("Agent API HTTP Server Shutdown Error: %v", err) + } +} + +func (a *AgentAPI) Process(message *core.Message) { + log.Tracef("Process function in the agent_api.go, %s %v", message.Topic(), message.Data()) +} + +func (a *AgentAPI) Info() *core.Info { + return core.NewInfo("Agent API Plugin", "v0.0.1") +} + +func (a *AgentAPI) Subscriptions() []string { + return []string{} +} + +func (a *AgentAPI) createHttpServer() { + mux := http.NewServeMux() + a.nginxHandler = &NginxHandler{a.env, a.nginxBinary} + mux.Handle("/nginx/", a.nginxHandler) + + log.Debug("Starting Agent API HTTP server") + + a.server = http.Server{ + Addr: fmt.Sprintf(":%d", a.config.AgentAPI.Port), + Handler: mux, + } + + if err := a.server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("error listening to port: %v", err) + } +} + +var ( + instancesRegex = regexp.MustCompile(`^\/nginx[\/]*$`) +) + +func (h *NginxHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + switch { + case r.Method == http.MethodGet && instancesRegex.MatchString(r.URL.Path): + err := sendInstanceDetailsPayload(h.getNginxDetails(), w, r) + if err != nil { + log.Warnf("Failed to send instance details payload: %v", err) + } + default: + w.WriteHeader(http.StatusNotFound) + _, err := fmt.Fprint(w, []byte("not found")) + if err != nil { + log.Warnf("Failed to send api response: %v", err) + } + } +} + +func sendInstanceDetailsPayload(nginxDetails []*proto.NginxDetails, w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(http.StatusOK) + + if len(nginxDetails) == 0 { + log.Debug("No nginx instances found") + _, err := fmt.Fprint(w, "[]") + if err != nil { + return fmt.Errorf("failed to send payload: %v", err) + } + + return nil + } + + respBody := new(bytes.Buffer) + err := json.NewEncoder(respBody).Encode(nginxDetails) + if err != nil { + return fmt.Errorf("failed to encode payload: %v", err) + } + + _, err = fmt.Fprint(w, respBody) + if err != nil { + return fmt.Errorf("failed to send payload: %v", err) + } + + return nil +} + +func (h *NginxHandler) getNginxDetails() []*proto.NginxDetails { + var nginxDetails []*proto.NginxDetails + + for _, proc := range h.env.Processes() { + if proc.IsMaster { + nginxDetails = append(nginxDetails, h.nginxBinary.GetNginxDetailsFromProcess(proc)) + } + } + + return nginxDetails +}