Skip to content

Commit

Permalink
Merge pull request #2 from umputun/program
Browse files Browse the repository at this point in the history
Program provider
  • Loading branch information
umputun authored Feb 6, 2022
2 parents b5e3919 + ee9af4e commit 5e04ec9
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 10 deletions.
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
7 changes: 4 additions & 3 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
10 changes: 7 additions & 3 deletions app/status/external/ext_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()}
Expand Down
16 changes: 13 additions & 3 deletions app/status/external/ext_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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)
}
67 changes: 67 additions & 0 deletions app/status/external/program.go
Original file line number Diff line number Diff line change
@@ -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
}
64 changes: 64 additions & 0 deletions app/status/external/program_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
3 changes: 3 additions & 0 deletions app/status/external/testdata/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env sh

echo "Hello, World!"

0 comments on commit 5e04ec9

Please sign in to comment.