Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin restapi: New plugin to provide a REST API. #1

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions restapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# restapi

Plugin **restapi** provides a REST API to communicate with the *collectd*
daemon.

## Description

The *restapi plugin* starts a webserver and waits for incoming REST API
requests.

## Building

To build this plugin, the collectd header files are required.

On Debian and Ubuntu, the collectd headers are available from the
`collectd-dev` package. Once installed, add the import paths to the
`CGI_CPPFLAGS`:

```bash
export CGO_CPPFLAGS="-I/usr/include/collectd/core/daemon \
-I/usr/include/collectd/core -I/usr/include/collectd"
```

Alternatively, you can grab the collectd sources, run the `configure` script,
and reference the header files from there:

```bash
TOP_SRCDIR="${HOME}/collectd"
export CGO_CPPFLAGS="-I${TOP_SRCDIR}/src -I${TOP_SRCDIR}/src/daemon"
```

Then build the plugin with the "c-shared" buildmode:

```bash
go build -buildmode=c-shared -o restapi.so
```

## Configuration

### Synopsis

```
LoadPlugin restapi
<Plugin "restapi">
Addr "::"
Port "8443"
CertFile "/path/to/cert_file.pem"
KeyFile "/path/to/key_file.pem"
</Plugin>
```

### Options

* **Addr** *Network address*

Addredd to listen to. Defaults to `""` (any address).
* **Port** *Port*

Post to listen to. Defaults to `8080` (`8443` if **CertFile** is specified).
* **CertFile** *Path*<br>
**KeyFile** *Path*

TLS certificate and key files. Refer to
[`"net/http".ListenAndServeTLS`](https://golang.org/pkg/net/http/#ListenAndServeTLS)
for more information on the TLS setup.
118 changes: 118 additions & 0 deletions restapi/restapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"time"

"collectd.org/api"
"collectd.org/config"
"collectd.org/plugin"
"go.uber.org/multierr"
)

const pluginName = "restapi"

type restapi struct {
srv *http.Server
}

func init() {
ra := &restapi{}

plugin.RegisterConfig(pluginName, ra)
plugin.RegisterShutdown(pluginName, ra)
}

func valueListHandler(w http.ResponseWriter, req *http.Request) {
if req.Method != http.MethodPost {
w.WriteHeader(http.StatusNotImplemented)
fmt.Fprintln(w, "Only POST is currently supported.")
return
}

var vls []api.ValueList
if err := json.NewDecoder(req.Body).Decode(&vls); err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, "parsing JSON failed:", err)
return
}

var errs error
for _, vl := range vls {
errs = multierr.Append(errs,
plugin.Write(req.Context(), &vl))
}

if errs != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "plugin.Write():", errs)
return
}
}

type srvConfig struct {
Args string // unused
Addr string
Port string
CertFile string
KeyFile string
}

func (ra *restapi) Configure(_ context.Context, rawConfig config.Block) error {
cfg := srvConfig{}
if err := rawConfig.Unmarshal(&cfg); err != nil {
return err
}

if (cfg.CertFile == "") != (cfg.KeyFile == "") {
return fmt.Errorf("CertFile=%q, KeyFile=%q; need both for TLS setup",
cfg.CertFile, cfg.KeyFile)
}

if cfg.Port == "" {
cfg.Port = "8080"
if cfg.CertFile != "" {
cfg.Port = "8443"
}
}

mux := http.NewServeMux()
mux.HandleFunc("/valueList", valueListHandler)

ra.srv = &http.Server{
Addr: net.JoinHostPort(cfg.Addr, cfg.Port),
Handler: mux,
}

go func() {
var err error
if cfg.CertFile != "" {
err = ra.srv.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)
} else {
err = ra.srv.ListenAndServe()
}
if !errors.Is(err, http.ErrServerClosed) {
plugin.Errorf("%s plugin: ListenAndServe(): %v", pluginName, err)
}
}()

return nil
}

func (ra *restapi) Shutdown(ctx context.Context) error {
if ra == nil || ra.srv == nil {
return nil
}

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

return ra.srv.Shutdown(ctx)
}

func main() {} // ignored