diff --git a/README.md b/README.md index 67da174..be9ff75 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,32 @@ Request examples: - `docker.body.unhealthy` - number of unhealthy containers, only for those with health check - `docker.body.required` - "ok" if all required containers are running, otherwise "failed" with a list of failed containers -## api +#### `program` provider + +This check runs any predefined program/script and checks the exit code. All commands are executed in shell. + +Request examples: +- `foo:program://ps?args=-ef` - runs `ps -ef` and checks exit code +- `bar:program:///tmp/foo/bar.sh` - runs /tmp/foo/bar.sh and checks exit code + +- Response example: + +```json +{ + "program": { + "name": "foo", + "status_code": 20, + "response_time": 44, + "body": { + "command": "ps -ef", + "stdout": "some output", + "status": "ok" + } + } +} +``` + +## API - `GET /status` - returns server status in JSON format - `GET /ping` - returns `pong` diff --git a/app/main.go b/app/main.go index 889fff9..5de3a12 100644 --- a/app/main.go +++ b/app/main.go @@ -68,9 +68,10 @@ func main() { } providers := external.Providers{ - HTTP: &external.HTTPProvider{Client: http.Client{Timeout: opts.TimeOut}}, - Mongo: &external.MongoProvider{TimeOut: opts.TimeOut}, - Docker: &external.DockerProvider{TimeOut: opts.TimeOut}, + HTTP: &external.HTTPProvider{Client: http.Client{Timeout: opts.TimeOut}}, + Mongo: &external.MongoProvider{TimeOut: opts.TimeOut}, + Docker: &external.DockerProvider{TimeOut: opts.TimeOut}, + Program: &external.ProgramProvider{TimeOut: opts.TimeOut, WithShell: true}, } srv := server.Rest{ diff --git a/app/status/external/ext_service.go b/app/status/external/ext_service.go index 3efa167..3ee99dc 100644 --- a/app/status/external/ext_service.go +++ b/app/status/external/ext_service.go @@ -22,9 +22,10 @@ type Service struct { // Providers is a list of StatusProvider type Providers struct { - HTTP StatusProvider - Mongo StatusProvider - Docker StatusProvider + HTTP StatusProvider + Mongo StatusProvider + Docker StatusProvider + Program StatusProvider } // StatusProvider is an interface for getting status from external services @@ -95,6 +96,9 @@ func (s *Service) Status() []Response { resp, err = s.providers.Mongo.Status(r) case strings.HasPrefix(r.URL, "docker://"): resp, err = s.providers.Docker.Status(r) + case strings.HasPrefix(r.URL, "program://"): + resp, err = s.providers.Program.Status(r) + default: log.Printf("[WARN] unsupported protocol for service, %s %s", r.Name, r.URL) ch <- Response{Name: r.Name, StatusCode: http.StatusInternalServerError, ResponseTime: time.Since(st).Milliseconds()} diff --git a/app/status/external/ext_service_test.go b/app/status/external/ext_service_test.go index fc256a8..0f2b51e 100644 --- a/app/status/external/ext_service_test.go +++ b/app/status/external/ext_service_test.go @@ -47,12 +47,16 @@ func TestService_Status(t *testing.T) { pd := &StatusProviderMock{StatusFunc: func(r Request) (*Response, error) { return &Response{StatusCode: 202, Name: "docker"}, nil }} + pp := &StatusProviderMock{StatusFunc: func(r Request) (*Response, error) { + return &Response{StatusCode: 203, Name: "program"}, nil + }} - s := NewService(Providers{ph, pm, pd}, 4, - "s1:http://127.0.0.1/ping", "s2:docker:///var/blah", "s3:mongodb://127.0.0.1:27017", "bad:bad") + s := NewService(Providers{ph, pm, pd, pp}, 4, + "s1:http://127.0.0.1/ping", "s2:docker:///var/blah", "s3:mongodb://127.0.0.1:27017", + "s4:program://ls?arg=1", "bad:bad") res := s.Status() - require.Equal(t, 4, len(res)) + require.Equal(t, 5, len(res)) assert.Equal(t, 1, len(ph.StatusCalls())) assert.Equal(t, Request{Name: "s1", URL: "http://127.0.0.1/ping"}, ph.StatusCalls()[0].Req) @@ -62,6 +66,9 @@ func TestService_Status(t *testing.T) { assert.Equal(t, 1, len(pd.StatusCalls())) assert.Equal(t, Request{Name: "s3", URL: "mongodb://127.0.0.1:27017"}, pm.StatusCalls()[0].Req) + assert.Equal(t, 1, len(pp.StatusCalls())) + assert.Equal(t, Request{Name: "s4", URL: "program://ls?arg=1"}, pp.StatusCalls()[0].Req) + assert.Equal(t, "bad", res[0].Name) assert.Equal(t, 500, res[0].StatusCode) @@ -73,4 +80,7 @@ func TestService_Status(t *testing.T) { assert.Equal(t, "mongo", res[3].Name) assert.Equal(t, 201, res[3].StatusCode) + + assert.Equal(t, "program", res[4].Name) + assert.Equal(t, 203, res[4].StatusCode) } diff --git a/app/status/external/program.go b/app/status/external/program.go new file mode 100644 index 0000000..2a3d8b0 --- /dev/null +++ b/app/status/external/program.go @@ -0,0 +1,67 @@ +package external + +import ( + "bytes" + "context" + "fmt" + "log" + "os" + "os/exec" + "strings" + "time" +) + +// ProgramProvider is an external service that runs a command and checks the exit code. +type ProgramProvider struct { + WithShell bool + TimeOut time.Duration +} + +// Status returns the status of the execution of the command from the request. +// url looks like this: program://cat?args=/tmp/foo +func (p *ProgramProvider) Status(req Request) (*Response, error) { + st := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), p.TimeOut) + defer cancel() + + resp := Response{ + Name: req.Name, + StatusCode: 200, + } + + command := strings.TrimPrefix(req.URL, "program://") + args := "" + if strings.Contains(command, "?args=") { + elems := strings.Split(command, "?args=") + command, args = elems[0], elems[1] + } + + log.Printf("[DEBUG] command: %s %s", command, args) + + cmd := exec.CommandContext(ctx, command, args) //nolint:gosec // we trust the command as it comes from the config + if p.WithShell { + command = fmt.Sprintf("sh -c %q", command+" "+args) + } + stdOut, stdErr := bytes.NewBuffer(nil), bytes.NewBuffer(nil) + cmd.Stdout = stdOut + cmd.Stderr = stdErr + cmd.Stdin = os.Stdin + + err := cmd.Run() + resp.ResponseTime = time.Since(st).Milliseconds() + + res := map[string]interface{}{ + "command": command + " " + args, + "stdout": stdOut.String(), + "stderr": stdErr.String(), + "status": "ok", + } + + if err != nil { + res["status"] = err.Error() + resp.StatusCode = 500 + } + + resp.Body = res + return &resp, nil +} diff --git a/app/status/external/program_test.go b/app/status/external/program_test.go new file mode 100644 index 0000000..42a1a9e --- /dev/null +++ b/app/status/external/program_test.go @@ -0,0 +1,64 @@ +package external + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProgram_StatusWithShell(t *testing.T) { + p := ProgramProvider{WithShell: true, TimeOut: time.Second} + + { + req := Request{Name: "test", URL: `program://ls?args=-la`} + resp, err := p.Status(req) + require.NoError(t, err) + assert.Equal(t, "test", resp.Name) + assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, "ok", resp.Body["status"]) + assert.Contains(t, resp.Body["stdout"], "program.go") + t.Logf("%+v", resp) + } + { + req := Request{Name: "test", URL: `program://testdata/test.sh`} + resp, err := p.Status(req) + require.NoError(t, err) + assert.Equal(t, "test", resp.Name) + assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, "ok", resp.Body["status"]) + assert.Contains(t, resp.Body["stdout"], "Hello, World!") + } + { + req := Request{Name: "test", URL: `program://blah?args=-la`} + resp, err := p.Status(req) + require.NoError(t, err) + assert.Equal(t, "test", resp.Name) + assert.Equal(t, 500, resp.StatusCode) + assert.Contains(t, resp.Body["status"], "file not found") + } +} + +func TestProgram_StatusWithoutShell(t *testing.T) { + p := ProgramProvider{WithShell: true, TimeOut: time.Second} + + { + req := Request{Name: "test", URL: `program://cat?args=program.go`} + resp, err := p.Status(req) + require.NoError(t, err) + assert.Equal(t, "test", resp.Name) + assert.Equal(t, 200, resp.StatusCode) + assert.Equal(t, "ok", resp.Body["status"]) + assert.Contains(t, resp.Body["stdout"], "CommandContext") + } + { + req := Request{Name: "test", URL: `program://cat?args=blah`} + resp, err := p.Status(req) + require.NoError(t, err) + assert.Equal(t, "test", resp.Name) + assert.Equal(t, 500, resp.StatusCode) + assert.Contains(t, resp.Body["status"], "exit status 1", resp.Body["status"]) + t.Logf("%+v", resp) + } +} diff --git a/app/status/external/testdata/test.sh b/app/status/external/testdata/test.sh new file mode 100755 index 0000000..7c2ccb9 --- /dev/null +++ b/app/status/external/testdata/test.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +echo "Hello, World!" \ No newline at end of file