(Translated by https://www.hiragana.jp/)
Add support of POST method (#18) · umputun/updater@06d55dc · GitHub
Skip to content

Commit

Permalink
Add support of POST method (#18)
Browse files Browse the repository at this point in the history
* add support of POST invocation

* bump deps and revendor

* missing v7 update for throttler

* lint: minor warns
  • Loading branch information
umputun authored Aug 7, 2022
1 parent cf2e377 commit 06d55dc
Show file tree
Hide file tree
Showing 53 changed files with 1,143 additions and 274 deletions.
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img class="logo" src="https://raw.githubusercontent.com/umputun/updater/master/site/src/logo-bg.svg" width="355px" height="142px" alt="Updater | Simple Remote Updater"/>
</div>

Updater is a simple web-hook-based receiver executing things via HTTP requests and invoking remote updates without exposing any sensitive info, like ssh keys, passwords, etc. The updater is usually called from CI/CD system (i.e., Github action), and the actual http call looks like `curl https://<server>/update/<task-name>/<access-key>`
Updater is a simple web-hook-based receiver executing things via HTTP requests and invoking remote updates without exposing any sensitive info, like ssh keys, passwords, etc. The updater is usually called from CI/CD system (i.e., Github action), and the actual http call looks like `curl https://<server>/update/<task-name>/<access-key>`. Alternatively, the updater can be called with POST method and the payload can be passed as JSON, i.e. `curl -X POST -d '{"task":"remark42-site", "secret":"123456"}' https://example.com/update`

List of tasks defined in the configuration file, and each task has its custom section for the command.

Expand Down Expand Up @@ -33,7 +33,7 @@ tasks:
docker restart feed-master
```
By default the update call synchronous but can be switched to non-blocking mode with `async` query parameter, i.e. `curl https://example.com/update/remark42-site/super-seecret-key?async=1`
By default the update call synchronous but can be switched to non-blocking mode with `async` query parameter, i.e. `curl https://example.com/update/remark42-site/super-seecret-key?async=1`. To request the async update with `POST`, `async=true` should be used in the payload, i.e. `curl -X POST -d '{"task":"remark42-site", "secret":"123456", "async":true}' https://example.com/update`

## Install

Expand Down Expand Up @@ -143,11 +143,14 @@ The main goal of this utility is to update containers; however, all it does is t
## All parameters

```
-f, --file= config file (default: updater.yml) [$CONF]
-l, --listen= listen on host:port (default: localhost:8080) [$LISTEN]
-k, --key= secret key [$KEY]
-b, --batch batch mode for multi-line scripts
--dbg show debug info
-f, --file= config file (default: updater.yml) [$CONF]
-l, --listen= listen on host:port (default: localhost:8080) [$LISTEN]
-k, --key= secret key [$KEY]
-b, --batch batch mode for multi-line scripts
--limit= limit how many concurrent update can be running (default: 10)
--timeout= for how long update task can be running (default: 1m)
--update-delay= delay between updates (default: 1s)
--dbg show debug info [$DEBUG]

Help Options:
-h, --help Show this help message
Expand Down
17 changes: 9 additions & 8 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ import (
var revision string

var opts struct {
Config string `short:"f" long:"file" env:"CONF" default:"updater.yml" description:"config file"`
Listen string `short:"l" long:"listen" env:"LISTEN" default:"localhost:8080" description:"listen on host:port"`
SecretKey string `short:"k" long:"key" env:"KEY" required:"true" description:"secret key"`
Batch bool `short:"b" long:"batch" description:"batch mode for multi-line scripts"`
Limit int `long:"limit" default:"10" description:"limit how many concurrent update can be running"`
TimeOut time.Duration `long:"timeout" default:"1m" description:"for how long update task can be running"`
Dbg bool `long:"dbg" env:"DEBUG" description:"show debug info"`
Config string `short:"f" long:"file" env:"CONF" default:"updater.yml" description:"config file"`
Listen string `short:"l" long:"listen" env:"LISTEN" default:"localhost:8080" description:"listen on host:port"`
SecretKey string `short:"k" long:"key" env:"KEY" required:"true" description:"secret key"`
Batch bool `short:"b" long:"batch" description:"batch mode for multi-line scripts"`
Limit int `long:"limit" default:"10" description:"limit how many concurrent update can be running"`
TimeOut time.Duration `long:"timeout" default:"1m" description:"for how long update task can be running"`
UpdateDelay time.Duration `long:"update-delay" default:"1s" description:"delay between updates"`
Dbg bool `long:"dbg" env:"DEBUG" description:"show debug info"`
}

func main() {
Expand Down Expand Up @@ -70,7 +71,7 @@ func main() {
SecretKey: opts.SecretKey,
Config: conf,
Runner: runner,
UpdateDelay: time.Second,
UpdateDelay: opts.UpdateDelay,
}

if err := srv.Run(ctx); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions app/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package main

import (
"fmt"
"io/ioutil"
"io"
"math/rand"
"net/http"
"os"
Expand Down Expand Up @@ -46,7 +46,7 @@ func Test_main(t *testing.T) {
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, 200, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "pong", string(body))
}
Expand Down
36 changes: 33 additions & 3 deletions app/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package server
import (
"context"
"crypto/subtle"
"encoding/json"
"io"
"net/http"
"time"

"github.com/didip/tollbooth/v6"
"github.com/didip/tollbooth/v7"
"github.com/didip/tollbooth_chi"
"github.com/go-chi/chi/v5"
log "github.com/go-pkgz/lgr"
Expand Down Expand Up @@ -70,19 +71,40 @@ func (s *Rest) router() http.Handler {
router.Use(rest.AppInfo("updater", "umputun", s.Version))
router.Use(rest.Ping)
router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
if s.UpdateDelay > 0 {
router.Use(s.slowMiddleware)
}

router.Get("/update/{task}/{key}", s.taskCtrl)
router.Post("/update", s.taskPostCtrl)
return router
}

// GET /update/{task}/{key}?async=[0|1]
func (s *Rest) taskCtrl(w http.ResponseWriter, r *http.Request) {
time.Sleep(s.UpdateDelay) // slow down the request
taskName := chi.URLParam(r, "task")
key := chi.URLParam(r, "key")
isAsync := r.URL.Query().Get("async") == "1" || r.URL.Query().Get("async") == "yes"
s.execTask(w, r, key, taskName, isAsync)
}

// POST /update
func (s *Rest) taskPostCtrl(w http.ResponseWriter, r *http.Request) {
req := struct {
Task string `json:"task"`
Secret string `json:"secret"`
Async bool `json:"async"`
}{}

if subtle.ConstantTimeCompare([]byte(key), []byte(s.SecretKey)) != 1 {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "failed to decode request", http.StatusBadRequest)
return
}
s.execTask(w, r, req.Secret, req.Task, req.Async)
}

func (s *Rest) execTask(w http.ResponseWriter, r *http.Request, secret, taskName string, isAsync bool) {
if subtle.ConstantTimeCompare([]byte(secret), []byte(s.SecretKey)) != 1 {
http.Error(w, "rejected", http.StatusForbidden)
return
}
Expand Down Expand Up @@ -113,3 +135,11 @@ func (s *Rest) taskCtrl(w http.ResponseWriter, r *http.Request) {

rest.RenderJSON(w, rest.JSON{"updated": "ok", "task": taskName})
}

// middleware for slowing requests downs
func (s *Rest) slowMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(s.UpdateDelay)
next.ServeHTTP(w, r)
})
}
39 changes: 39 additions & 0 deletions app/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -82,3 +83,41 @@ func TestRest_taskCtrlAsync(t *testing.T) {
assert.True(t, time.Since(st) < 100*time.Millisecond, time.Since(st))
time.Sleep(100 * time.Millisecond)
}

func TestRest_taskPostCtrl(t *testing.T) {
conf := &mocks.ConfigMock{GetTaskCommandFunc: func(name string) (string, bool) {
return "echo " + name, true
}}

runner := &mocks.RunnerMock{RunFunc: func(ctx context.Context, command string, logWriter io.Writer) error {
return nil
}}

srv := Rest{Listen: "localhost:54009", Version: "v1", Config: conf, SecretKey: "12345",
Runner: runner, UpdateDelay: time.Millisecond * 200}

ts := httptest.NewServer(srv.router())
defer ts.Close()

st := time.Now()
resp, err := http.Post(ts.URL+"/update", "application/json", strings.NewReader(`{"task":"task1","secret":"12345"}`))

require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

resp, err = http.Post(ts.URL+"/update", "application/json", strings.NewReader(`{"task":"task2","secret":"12345"}`))
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

resp, err = http.Post(ts.URL+"/update", "application/json", strings.NewReader(`{"task":"task2","secret":"12345bad"}`))
require.NoError(t, err)
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
assert.True(t, time.Since(st) >= time.Millisecond*200)
assert.Equal(t, 2, len(conf.GetTaskCommandCalls()))
assert.Equal(t, "task1", conf.GetTaskCommandCalls()[0].Name)
assert.Equal(t, "task2", conf.GetTaskCommandCalls()[1].Name)

assert.Equal(t, 2, len(runner.RunCalls()))
assert.Equal(t, "echo task1", runner.RunCalls()[0].Command)
assert.Equal(t, "echo task2", runner.RunCalls()[1].Command)
}
5 changes: 2 additions & 3 deletions app/task/shell_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
Expand Down Expand Up @@ -99,7 +98,7 @@ func (s *ShellRunner) prepBatch(cmd string) (batchFile string, err error) {
var script []string
script = append(script, "#!bin/sh")
script = append(script, strings.Split(cmd, "\n")...)
fh, e := ioutil.TempFile("/tmp", "updater")
fh, e := os.CreateTemp("/tmp", "updater")
if e != nil {
return "", errors.Wrap(e, "failed to prep batch")
}
Expand All @@ -108,7 +107,7 @@ func (s *ShellRunner) prepBatch(cmd string) (batchFile string, err error) {
fname := fh.Name()
errs = multierror.Append(errs, fh.Sync())
errs = multierror.Append(errs, fh.Close())
errs = multierror.Append(errs, os.Chmod(fname, 0755)) //nolint
errs = multierror.Append(errs, os.Chmod(fname, 0755)) // nolint
if errs.ErrorOrNil() != nil {
log.Printf("[WARN] can't properly close %s, %v", fname, errs.Error())
}
Expand Down
17 changes: 9 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ module github.com/umputun/updater
go 1.17

require (
github.com/didip/tollbooth/v6 v6.1.1
github.com/didip/tollbooth_chi v0.0.0-20200828173446-a7173453ea21
github.com/go-chi/chi/v5 v5.0.4
github.com/didip/tollbooth/v7 v7.0.0
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f
github.com/go-chi/chi/v5 v5.0.7
github.com/go-pkgz/lgr v0.10.4
github.com/go-pkgz/rest v1.11.0
github.com/go-pkgz/syncs v1.1.1
github.com/go-pkgz/rest v1.15.6
github.com/go-pkgz/syncs v1.2.0
github.com/hashicorp/go-multierror v1.1.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.7.1
github.com/umputun/go-flags v1.5.1
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/go-pkgz/expirable-cache v0.0.3 // indirect
github.com/go-pkgz/expirable-cache v0.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
34 changes: 14 additions & 20 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/didip/tollbooth/v6 v6.0.1/go.mod h1:j2pKs+JQ5PvU/K4jFnrnwntrmfUbYLJE5oSdxR37FD0=
github.com/didip/tollbooth/v6 v6.1.1 h1:Nt7PvWLa9Y94OrykXsFNBinVRQIu8xdy4avpl99Dc1M=
github.com/didip/tollbooth/v6 v6.1.1/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s=
github.com/didip/tollbooth_chi v0.0.0-20200828173446-a7173453ea21 h1:x7YpwKSBIBcKe9I3aTNOqgSyJ6QKDdtOxnEkxBTsi9w=
github.com/didip/tollbooth_chi v0.0.0-20200828173446-a7173453ea21/go.mod h1:0ZVa6kSzS011nfTC1rELyxK4tjVf6vqBnOv7oY2KlsA=
github.com/go-chi/chi/v5 v5.0.4 h1:5e494iHzsYBiyXQAHHuI4tyJS9M3V84OuX3ufIIGHFo=
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc=
github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ=
github.com/didip/tollbooth/v7 v7.0.0 h1:XmyyNwZpz9j61PwR4A894MmmYO5zBF9xjgVi2n1fiQI=
github.com/didip/tollbooth/v7 v7.0.0/go.mod h1:VZhDSGl5bDSPj4wPsih3PFa4Uh9Ghv8hgacaTm5PRT4=
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f h1:jtKwihcLmUC9BAhoJ9adCUqdSSZcOdH2KL7mPTUm2aw=
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C80dnsuVRP2dAskjnXRNWdUJqtGgwG9wNrzt0019s=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-pkgz/expirable-cache v0.1.0 h1:3bw0m8vlTK8qlwz5KXuygNBTkiKRTPrAGXU0Ej2AC1g=
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
github.com/go-pkgz/lgr v0.10.4 h1:l7qyFjqEZgwRgaQQSEp6tve4A3OU80VrfzpvtEX8ngw=
github.com/go-pkgz/lgr v0.10.4/go.mod h1:CD0s1z6EFpIUplV067gitF77tn25JItzwHNKAPqeCF0=
github.com/go-pkgz/rest v1.11.0 h1:Z//qgmM0NhBYfhXYEP/aJtDVLK5XlJGxqcb4sHFNN0E=
github.com/go-pkgz/rest v1.11.0/go.mod h1:wZ/dGipZUaF9to0vIQl7PwDHgWQDB0jsrFg1xnAKLDw=
github.com/go-pkgz/syncs v1.1.1 h1:jWN+y6FS/Xe+8z4l3QMbSnODGyaxDHGojIS+wyKIjxg=
github.com/go-pkgz/syncs v1.1.1/go.mod h1:bt9lxWRRJ9vOCMGc8Big8ttjYHLKP88ofj1y38UlaHE=
github.com/go-pkgz/rest v1.15.6 h1:8RgOuY/c00CD0el8KdmscOCgDH+ML0ZsK2qa1Rcxal4=
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
github.com/go-pkgz/syncs v1.2.0 h1:aiizQFILlMZ4KtRNaYLcDffRbUQZH9fclsgr5KybWyY=
github.com/go-pkgz/syncs v1.2.0/go.mod h1:fjThZdM2FkC/oSeiqBTOZOtHpbrCh4HuHbipB5qZJJM=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
Expand All @@ -30,20 +29,15 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/umputun/go-flags v1.5.1 h1:vRauoXV3Ultt1HrxivSxowbintgZLJE+EcBy5ta3/mY=
github.com/umputun/go-flags v1.5.1/go.mod h1:nTbvsO/hKqe7Utri/NoyN18GR3+EWf+9RrmsdwdhrEc=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
Expand Down
55 changes: 0 additions & 55 deletions vendor/github.com/didip/tollbooth/v6/libstring/libstring.go

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 06d55dc

Please sign in to comment.